tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See CLI examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from dateutil.tz import tzlocal 41from time import sleep 42 43import re 44import json 45import requests 46import traceback as tb 47from typing import Union 48 49from multiprocessing import cpu_count, Lock 50from multiprocessing.pool import ThreadPool 51import pandas as pd 52 53from mako.template import Template # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 54from Templates import * # Some html-templates used by reporting methods in TKSBrokerAPI module 55from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 56from TradeRoutines import * # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module 57 58from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator) 59from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 60 61import UniLogger as uLog # Logger for TKSBrokerAPI 62 63 64# --- Common technical parameters: 65 66PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 67uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 68uLogger.level = 10 # debug level by default for TKSBrokerAPI module 69uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 70 71__version__ = "1.6" # The "major.minor" version setup here, but build number define at the build-server only 72 73CPU_COUNT = cpu_count() # host's real CPU count 74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 75 76 77class TinkoffBrokerServer: 78 """ 79 This class implements methods to work with Tinkoff broker server. 80 81 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 82 83 About `token`: https://tinkoff.github.io/investAPI/token/ 84 """ 85 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 86 """ 87 Main class init. 88 89 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 90 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 91 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 92 :param useCache: use default cache file with raw data to use instead of `iList`. 93 True by default. Cache is auto-update if new day has come. 94 If you don't want to use cache and always updates raw data then set `useCache=False`. 95 :param defaultCache: path to default cache file. `dump.json` by default. 96 """ 97 if token is None or not token: 98 try: 99 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 100 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 101 102 except KeyError: 103 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 104 raise Exception("Token required") 105 106 else: 107 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 108 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 109 110 if accountId is None or not accountId: 111 try: 112 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 113 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 114 115 except KeyError: 116 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 117 118 else: 119 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 120 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 121 122 self.version = __version__ # duplicate here used TKSBrokerAPI main version 123 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 124 125 Latest version: https://pypi.org/project/tksbrokerapi/ 126 """ 127 128 self.__lock = Lock() # initialize multiprocessing mutex lock 129 130 self.aliases = TKS_TICKER_ALIASES 131 """Some aliases instead official tickers. 132 133 See also: `TKSEnums.TKS_TICKER_ALIASES` 134 """ 135 136 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 137 138 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 139 140 self._ticker = "" 141 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 142 143 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 144 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 145 146 See also: `SearchByTicker()`, `SearchInstruments()`. 147 """ 148 149 self._figi = "" 150 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 151 152 See also: `SearchByFIGI()`, `SearchInstruments()`. 153 """ 154 155 self.depth = 1 156 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 157 158 See also: `GetCurrentPrices()`. 159 """ 160 161 self.server = r"https://invest-public-api.tinkoff.ru/rest" 162 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 163 164 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 165 """ 166 167 uLogger.debug("Broker API server: {}".format(self.server)) 168 169 self.timeout = 15 170 """Server operations timeout in seconds. Default: `15`. 171 172 See also: `SendAPIRequest()`. 173 """ 174 175 self.headers = { 176 "Content-Type": "application/json", 177 "accept": "application/json", 178 "Authorization": "Bearer {}".format(self.token), 179 "x-app-name": "Tim55667757.TKSBrokerAPI", 180 } 181 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 182 183 See also: `SendAPIRequest()`. 184 """ 185 186 self.body = None 187 """Request body which send to broker server. Default: `None`. 188 189 See also: `SendAPIRequest()`. 190 """ 191 192 self.moreDebug = False 193 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 194 195 self.useHTMLReports = False 196 """ 197 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 198 199 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 200 """ 201 202 self.historyFile = None 203 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 204 205 See also: `History()`. 206 """ 207 208 self.htmlHistoryFile = "index.html" 209 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 210 211 See also: `ShowHistoryChart()`. 212 """ 213 214 self.instrumentsFile = "instruments.md" 215 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 216 217 See also: `ShowInstrumentsInfo()`. 218 """ 219 220 self.searchResultsFile = "search-results.md" 221 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 222 223 See also: `SearchInstruments()`. 224 """ 225 226 self.pricesFile = "prices.md" 227 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 228 229 See also: `GetListOfPrices()`. 230 """ 231 232 self.infoFile = "info.md" 233 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 234 235 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 236 """ 237 238 self.bondsXLSXFile = "ext-bonds.xlsx" 239 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 240 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 241 242 See also: `ExtendBondsData()`. 243 """ 244 245 self.calendarFile = "calendar.md" 246 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 247 248 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 249 250 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 251 """ 252 253 self.overviewFile = "overview.md" 254 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 255 256 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 257 """ 258 259 self.overviewDigestFile = "overview-digest.md" 260 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 261 262 See also: `Overview()` with parameter `details="digest"`. 263 """ 264 265 self.overviewPositionsFile = "overview-positions.md" 266 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 267 268 See also: `Overview()` with parameter `details="positions"`. 269 """ 270 271 self.overviewOrdersFile = "overview-orders.md" 272 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 273 274 See also: `Overview()` with parameter `details="orders"`. 275 """ 276 277 self.overviewAnalyticsFile = "overview-analytics.md" 278 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 279 280 See also: `Overview()` with parameter `details="analytics"`. 281 """ 282 283 self.overviewBondsCalendarFile = "overview-calendar.md" 284 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 285 286 See also: `Overview()` with parameter `details="calendar"`. 287 """ 288 289 self.reportFile = "deals.md" 290 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 291 292 See also: `Deals()`. 293 """ 294 295 self.withdrawalLimitsFile = "limits.md" 296 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 297 298 See also: `OverviewLimits()` and `RequestLimits()`. 299 """ 300 301 self.userInfoFile = "user-info.md" 302 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 303 304 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 305 """ 306 307 self.userAccountsFile = "accounts.md" 308 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 309 310 See also: `OverviewAccounts()`, `RequestAccounts()`. 311 """ 312 313 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 314 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 315 316 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 317 318 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 319 """ 320 321 self.iList = None # init iList for raw instruments data 322 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 323 324 See also: `Listing()`, `DumpInstruments()`. 325 """ 326 327 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 328 if useCache: 329 if os.path.exists(self.iListDumpFile): 330 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 331 curTime = datetime.now(tzutc()) 332 333 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 334 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 335 336 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 337 338 else: 339 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 340 341 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 342 os.path.abspath(self.iListDumpFile), 343 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 344 )) 345 346 else: 347 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 348 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 349 350 else: 351 self.iList = self.Listing() # request new raw instruments data from broker server 352 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 353 354 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 355 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 356 357 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 358 """ 359 360 @property 361 def ticker(self) -> str: 362 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 363 364 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 365 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 366 367 See also: `SearchByTicker()`, `SearchInstruments()`. 368 """ 369 return self._ticker 370 371 @ticker.setter 372 def ticker(self, value): 373 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 374 375 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 376 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 377 378 See also: `SearchByTicker()`, `SearchInstruments()`. 379 """ 380 self._ticker = str(value).upper() # Tickers may be upper case only 381 382 @property 383 def figi(self) -> str: 384 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 385 386 See also: `SearchByFIGI()`, `SearchInstruments()`. 387 """ 388 return self._figi 389 390 @figi.setter 391 def figi(self, value): 392 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 393 394 See also: `SearchByFIGI()`, `SearchInstruments()`. 395 """ 396 self._figi = str(value).upper() # FIGI may be upper case only 397 398 def _ParseJSON(self, rawData="{}") -> dict: 399 """ 400 Parse JSON from response string. 401 402 :param rawData: this is a string with JSON-formatted text. 403 :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`. 404 """ 405 try: 406 responseJSON = json.loads(rawData) if rawData else {} 407 408 if self.moreDebug: 409 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 410 411 return responseJSON 412 413 except Exception as e: 414 uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e)) 415 416 return {} 417 418 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 419 """ 420 Send GET or POST request to broker server and receive JSON object. 421 422 self.header: must be defining with dictionary of headers. 423 self.body: if define then used as request body. None by default. 424 self.timeout: global request timeout, 15 seconds by default. 425 :param url: url with REST request. 426 :param reqType: send "GET" or "POST" request. "GET" by default. 427 :param retry: how many times retry after first request if an 5xx server errors occurred. 428 :param pause: sleep time in seconds between retries. 429 :return: response JSON (dictionary) from broker. 430 """ 431 if reqType.upper() not in ("GET", "POST"): 432 uLogger.error("You can define request type: `GET` or `POST`!") 433 raise Exception("Incorrect value") 434 435 if self.moreDebug: 436 uLogger.debug("Request parameters:") 437 uLogger.debug(" - REST API URL: {}".format(url)) 438 uLogger.debug(" - request type: {}".format(reqType)) 439 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 440 uLogger.debug(" - body:\n{}".format(self.body)) 441 442 # fast hack to avoid all operations with some tickers/FIGI 443 responseJSON = {} 444 oK = True 445 for item in self.exclude: 446 if item in url: 447 if self.moreDebug: 448 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 449 450 oK = False 451 break 452 453 if oK: 454 with self.__lock: # acquire the mutex lock 455 counter = 0 456 response = None 457 errMsg = "" 458 459 while not response and counter <= retry: 460 if reqType == "GET": 461 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 462 463 if reqType == "POST": 464 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 465 466 if self.moreDebug: 467 uLogger.debug("Response:") 468 uLogger.debug(" - status code: {}".format(response.status_code)) 469 uLogger.debug(" - reason: {}".format(response.reason)) 470 uLogger.debug(" - body length: {}".format(len(response.text))) 471 uLogger.debug(" - headers:\n{}".format(response.headers)) 472 473 # Server returns some headers: 474 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 475 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 476 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 477 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 478 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 479 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 480 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 481 sleep(rateLimitWait) 482 483 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 484 if 400 <= response.status_code < 500: 485 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 486 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 487 488 if "code" in response.text and "message" in response.text: 489 msgDict = self._ParseJSON(rawData=response.text) 490 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 491 492 counter = retry + 1 # do not retry for 4xx errors 493 494 if 500 <= response.status_code < 600: 495 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 496 uLogger.debug(" - not oK, {}".format(errMsg)) 497 498 if "code" in response.text and "message" in response.text: 499 errMsgDict = self._ParseJSON(rawData=response.text) 500 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 501 502 counter += 1 503 504 if counter <= retry: 505 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 506 sleep(pause) 507 508 responseJSON = self._ParseJSON(rawData=response.text) 509 510 if errMsg: 511 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 512 uLogger.error(" - not oK, {}".format(errMsg)) 513 514 return responseJSON 515 516 def _IUpdater(self, iType: str) -> tuple: 517 """ 518 Request instrument by type from server. See available API methods for instruments: 519 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 520 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 521 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 522 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 523 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 524 525 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 526 :return: tuple with iType name and list of available instruments of current type for defined user token. 527 """ 528 result = [] 529 530 if iType in TKS_INSTRUMENTS: 531 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 532 533 # all instruments have the same body in API v2 requests: 534 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 535 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 536 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 537 538 return iType, result 539 540 def _IWrapper(self, kwargs): 541 """ 542 Wrapper runs instrument's update method `_IUpdater()`. 543 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 544 """ 545 return self._IUpdater(**kwargs) 546 547 def Listing(self) -> dict: 548 """ 549 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 550 551 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 552 """ 553 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 554 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 555 556 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 557 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 558 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 559 560 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 561 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 562 poolUpdater.close() # close the thread pool 563 poolUpdater.join() # wait a moment until all data returns from threads 564 565 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 566 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 567 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 568 569 # calculate minimum price increment (step) for all instruments and set up instrument's type: 570 for iType in iList.keys(): 571 for ticker in iList[iType]: 572 iList[iType][ticker]["type"] = iType 573 574 if "minPriceIncrement" in iList[iType][ticker].keys(): 575 iList[iType][ticker]["step"] = NanoToFloat( 576 iList[iType][ticker]["minPriceIncrement"]["units"], 577 iList[iType][ticker]["minPriceIncrement"]["nano"], 578 ) 579 580 else: 581 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 582 583 return iList 584 585 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 586 """ 587 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 588 589 See also: `DumpInstruments()`, `Listing()`. 590 591 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 592 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 593 """ 594 if self.iListDumpFile is None or not self.iListDumpFile: 595 uLogger.error("Output name of dump file must be defined!") 596 raise Exception("Filename required") 597 598 if not self.iList or forceUpdate: 599 self.iList = self.Listing() 600 601 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 602 603 # Save as XLSX with separated sheets for every type of instruments: 604 with pd.ExcelWriter( 605 path=xlsxDumpFile, 606 date_format=TKS_DATE_FORMAT, 607 datetime_format=TKS_DATE_TIME_FORMAT, 608 mode="w", 609 ) as writer: 610 for iType in TKS_INSTRUMENTS: 611 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 612 df = df[sorted(df)] # sorted by column names 613 df = df.applymap( 614 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 615 na_action="ignore", 616 ) # converting numbers from nano-type to float in every cell 617 df.to_excel( 618 writer, 619 sheet_name=iType, 620 encoding="UTF-8", 621 freeze_panes=(1, 1), 622 ) # saving as XLSX-file with freeze first row and column as headers 623 624 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 625 626 def DumpInstruments(self, forceUpdate: bool = True) -> str: 627 """ 628 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 629 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 630 631 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 632 633 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 634 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 635 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 636 """ 637 if self.iListDumpFile is None or not self.iListDumpFile: 638 uLogger.error("Output name of dump file must be defined!") 639 raise Exception("Filename required") 640 641 if not self.iList or forceUpdate: 642 self.iList = self.Listing() 643 644 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 645 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 646 fH.write(jsonDump) 647 648 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 649 650 return jsonDump 651 652 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 653 """ 654 Show information about one instrument defined by json data and prints it in Markdown format. 655 656 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 657 658 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 659 :param show: if `True` then also printing information about instrument and its current price. 660 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 661 :return: multilines text in Markdown format with information about one instrument. 662 """ 663 splitLine = "| | |\n" 664 infoText = "" 665 666 if iJSON is not None and iJSON and isinstance(iJSON, dict): 667 info = [ 668 "# Main information\n\n", 669 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 670 "| Parameters | Values |\n", 671 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 672 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 673 "| Full name: | {:<54} |\n".format(iJSON["name"]), 674 ] 675 676 if "sector" in iJSON.keys() and iJSON["sector"]: 677 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 678 679 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 680 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 681 682 info.extend([ 683 splitLine, 684 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 685 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 686 ]) 687 688 if "isin" in iJSON.keys() and iJSON["isin"]: 689 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 690 691 if "classCode" in iJSON.keys(): 692 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 693 694 info.extend([ 695 splitLine, 696 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 697 splitLine, 698 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 699 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 700 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 701 ]) 702 703 if iJSON["figi"]: 704 self._figi = iJSON["figi"] 705 iJSON = iJSON | self.RequestTradingStatus() 706 707 info.extend([ 708 splitLine, 709 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 710 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 711 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 712 ]) 713 714 info.append(splitLine) 715 716 if "type" in iJSON.keys() and iJSON["type"]: 717 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 718 719 if "shareType" in iJSON.keys() and iJSON["shareType"]: 720 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 721 722 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 723 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 724 725 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 726 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 727 728 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 729 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 730 731 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 732 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 733 734 if "focusType" in iJSON.keys() and iJSON["focusType"]: 735 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 736 737 if "assetType" in iJSON.keys() and iJSON["assetType"]: 738 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 739 740 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 741 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 742 743 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 744 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 745 746 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 747 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 748 749 if "currency" in iJSON.keys(): 750 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 751 752 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 753 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 754 755 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 756 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 757 758 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 759 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 760 761 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 762 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 763 764 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 765 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 766 767 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 768 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 769 770 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 771 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 772 773 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 774 info.append("| Perpetual bond: | Yes |\n") 775 776 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 777 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 778 779 iExt = None 780 if iJSON["type"] == "Bonds": 781 info.extend([ 782 splitLine, 783 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 784 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 785 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 786 iJSON["nominal"]["currency"], 787 )), 788 ]) 789 790 if "floatingCouponFlag" in iJSON.keys(): 791 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 792 793 if "amortizationFlag" in iJSON.keys(): 794 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 795 796 info.append(splitLine) 797 798 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 799 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 800 801 if iJSON["figi"]: 802 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 803 804 info.extend([ 805 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 806 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 807 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 808 ]) 809 810 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 811 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 812 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 813 iJSON["aciValue"]["currency"] 814 ))) 815 816 if "currentPrice" in iJSON.keys(): 817 info.append(splitLine) 818 819 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 820 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 821 822 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 823 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 824 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 825 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 826 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 827 828 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 829 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 830 831 info.extend([ 832 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 833 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 834 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 835 )), 836 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 837 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 838 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 839 )), 840 "| Changes between last deal price and last close | {:<54} |\n".format( 841 "{:.2f}%{}".format( 842 iJSON["currentPrice"]["changes"], 843 " ({}{:.2f} {})".format( 844 "+" if bondChangesDelta > 0 else "", 845 bondChangesDelta, 846 aciCurrency 847 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 848 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 849 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 850 currency 851 ), 852 ) 853 ), 854 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 855 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 856 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 857 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 858 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 859 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 860 )), 861 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 862 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 863 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 864 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 865 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 866 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 867 )), 868 ]) 869 870 if "lot" in iJSON.keys(): 871 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 872 873 if "step" in iJSON.keys() and iJSON["step"] != 0: 874 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 875 876 # Add bond payment calendar: 877 if iJSON["type"] == "Bonds": 878 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 879 info.extend(["\n#", strCalendar]) 880 881 infoText += "".join(info) 882 883 if show and not onlyFiles: 884 uLogger.info("{}".format(infoText)) 885 886 if self.infoFile is not None and (show or onlyFiles): 887 with open(self.infoFile, "w", encoding="UTF-8") as fH: 888 fH.write(infoText) 889 890 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 891 892 if self.useHTMLReports: 893 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 894 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 895 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 896 897 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 898 899 return infoText 900 901 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 902 """ 903 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 904 905 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 906 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 907 :return: JSON formatted data with information about instrument. 908 """ 909 tickerJSON = {} 910 if self.moreDebug: 911 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 912 913 if not self._ticker: 914 uLogger.warning("self._ticker variable is not be empty!") 915 916 else: 917 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 918 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 919 raise Exception("Instrument not allowed") 920 921 if not self.iList: 922 self.iList = self.Listing() 923 924 if self._ticker in self.iList["Shares"].keys(): 925 tickerJSON = self.iList["Shares"][self._ticker] 926 if self.moreDebug: 927 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 928 929 elif self._ticker in self.iList["Currencies"].keys(): 930 tickerJSON = self.iList["Currencies"][self._ticker] 931 if self.moreDebug: 932 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 933 934 elif self._ticker in self.iList["Bonds"].keys(): 935 tickerJSON = self.iList["Bonds"][self._ticker] 936 if self.moreDebug: 937 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 938 939 elif self._ticker in self.iList["Etfs"].keys(): 940 tickerJSON = self.iList["Etfs"][self._ticker] 941 if self.moreDebug: 942 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 943 944 elif self._ticker in self.iList["Futures"].keys(): 945 tickerJSON = self.iList["Futures"][self._ticker] 946 if self.moreDebug: 947 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 948 949 if tickerJSON: 950 self._figi = tickerJSON["figi"] 951 952 if requestPrice: 953 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 954 955 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 956 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 957 958 else: 959 tickerJSON["currentPrice"]["changes"] = 0 960 961 if show: 962 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 963 964 else: 965 if show: 966 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 967 968 return tickerJSON 969 970 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 971 """ 972 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 973 974 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 975 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 976 :return: JSON formatted data with information about instrument. 977 """ 978 figiJSON = {} 979 if self.moreDebug: 980 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 981 982 if not self._figi: 983 uLogger.warning("self._figi variable is not be empty!") 984 985 else: 986 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 987 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 988 raise Exception("Instrument not allowed") 989 990 if not self.iList: 991 self.iList = self.Listing() 992 993 for item in self.iList["Shares"].keys(): 994 if self._figi == self.iList["Shares"][item]["figi"]: 995 figiJSON = self.iList["Shares"][item] 996 997 if self.moreDebug: 998 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 999 1000 break 1001 1002 if not figiJSON: 1003 for item in self.iList["Currencies"].keys(): 1004 if self._figi == self.iList["Currencies"][item]["figi"]: 1005 figiJSON = self.iList["Currencies"][item] 1006 1007 if self.moreDebug: 1008 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1009 1010 break 1011 1012 if not figiJSON: 1013 for item in self.iList["Bonds"].keys(): 1014 if self._figi == self.iList["Bonds"][item]["figi"]: 1015 figiJSON = self.iList["Bonds"][item] 1016 1017 if self.moreDebug: 1018 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1019 1020 break 1021 1022 if not figiJSON: 1023 for item in self.iList["Etfs"].keys(): 1024 if self._figi == self.iList["Etfs"][item]["figi"]: 1025 figiJSON = self.iList["Etfs"][item] 1026 1027 if self.moreDebug: 1028 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1029 1030 break 1031 1032 if not figiJSON: 1033 for item in self.iList["Futures"].keys(): 1034 if self._figi == self.iList["Futures"][item]["figi"]: 1035 figiJSON = self.iList["Futures"][item] 1036 1037 if self.moreDebug: 1038 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1039 1040 break 1041 1042 if figiJSON: 1043 self._figi = figiJSON["figi"] 1044 self._ticker = figiJSON["ticker"] 1045 1046 if requestPrice: 1047 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1048 1049 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1050 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1051 1052 else: 1053 figiJSON["currentPrice"]["changes"] = 0 1054 1055 if show: 1056 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1057 1058 else: 1059 if show: 1060 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1061 1062 return figiJSON 1063 1064 def GetCurrentPrices(self, show: bool = True) -> dict: 1065 """ 1066 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1067 `{"buy": [{"price": 1243.8, "quantity": 193}, 1068 {"price": 1244.0, "quantity": 168}, 1069 {"price": 1244.8, "quantity": 5}, 1070 {"price": 1245.0, "quantity": 61}, 1071 {"price": 1245.4, "quantity": 60}], 1072 "sell": [{"price": 1243.6, "quantity": 8}, 1073 {"price": 1242.6, "quantity": 10}, 1074 {"price": 1242.4, "quantity": 18}, 1075 {"price": 1242.2, "quantity": 50}, 1076 {"price": 1242.0, "quantity": 113}], 1077 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1078 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1079 - sell: list of dicts with Buyers prices, 1080 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1081 - quantity: volume value by current price in lots, 1082 - limitUp: current trade session limit price, maximum, 1083 - limitDown: current trade session limit price, minimum, 1084 - lastPrice: last deal price of the instrument, 1085 - closePrice: previous trade session close price of the instrument. 1086 1087 See also: `SearchByTicker()` and `SearchByFIGI()`. 1088 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1089 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1090 1091 :param show: if `True` then print DOM to log and console. 1092 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1093 If an error occurred then returns an empty record: 1094 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1095 """ 1096 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1097 1098 if self.depth < 1: 1099 uLogger.error("Depth of Market (DOM) must be >=1!") 1100 raise Exception("Incorrect value") 1101 1102 if not (self._ticker or self._figi): 1103 uLogger.error("self._ticker or self._figi variables must be defined!") 1104 raise Exception("Ticker or FIGI required") 1105 1106 if self._ticker and not self._figi: 1107 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1108 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1109 1110 if not self._ticker and self._figi: 1111 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1112 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1113 1114 if not self._figi: 1115 uLogger.error("FIGI is not defined!") 1116 raise Exception("Ticker or FIGI required") 1117 1118 else: 1119 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1120 1121 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1122 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1123 self.body = str({"figi": self._figi, "depth": self.depth}) 1124 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1125 1126 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1127 # list of dicts with sellers orders: 1128 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1129 1130 # list of dicts with buyers orders: 1131 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1132 1133 # max price of instrument at this time: 1134 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1135 1136 # min price of instrument at this time: 1137 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1138 1139 # last price of deal with instrument: 1140 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1141 1142 # last close price of instrument: 1143 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1144 1145 else: 1146 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1147 uLogger.debug("Server response: {}".format(pricesResponse)) 1148 1149 if show: 1150 if prices["buy"] or prices["sell"]: 1151 info = [ 1152 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1153 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1154 self._ticker, 1155 self._figi, 1156 self.depth, 1157 ), 1158 "-" * 60, "\n", 1159 " Orders of Buyers | Orders of Sellers\n", 1160 "-" * 60, "\n", 1161 " Sell prices (volumes) | Buy prices (volumes)\n", 1162 "-" * 60, "\n", 1163 ] 1164 1165 if not prices["buy"]: 1166 info.append(" | No orders!\n") 1167 sumBuy = 0 1168 1169 else: 1170 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1171 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1172 for item in maxMinSorted: 1173 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1174 1175 if not prices["sell"]: 1176 info.append("No orders! |\n") 1177 sumSell = 0 1178 1179 else: 1180 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1181 for item in prices["sell"]: 1182 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1183 1184 info.extend([ 1185 "-" * 60, "\n", 1186 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1187 "-" * 60, "\n", 1188 ]) 1189 1190 infoText = "".join(info) 1191 1192 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1193 1194 else: 1195 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1196 1197 return prices 1198 1199 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1200 """ 1201 This method get and show information about all available broker instruments for current user account. 1202 If `instrumentsFile` string is not empty then also save information to this file. 1203 1204 :param show: if `True` then print results to console, if `False` — print only to file. 1205 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1206 :return: multi-lines string with all available broker instruments. 1207 """ 1208 if not self.iList: 1209 self.iList = self.Listing() 1210 1211 info = [ 1212 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1213 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1214 ] 1215 1216 # add instruments count by type: 1217 for iType in self.iList.keys(): 1218 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1219 1220 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1221 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1222 1223 # generating info tables with all instruments by type: 1224 for iType in self.iList.keys(): 1225 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1226 1227 for instrument in self.iList[iType].keys(): 1228 iName = self.iList[iType][instrument]["name"] # instrument's name 1229 if len(iName) > 57: 1230 iName = "{}...".format(iName[:54]) # right trim for a long string 1231 1232 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1233 self.iList[iType][instrument]["ticker"], 1234 iName, 1235 self.iList[iType][instrument]["figi"], 1236 self.iList[iType][instrument]["currency"], 1237 self.iList[iType][instrument]["lot"], 1238 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1239 )) 1240 1241 infoText = "".join(info) 1242 1243 if show and not onlyFiles: 1244 uLogger.info(infoText) 1245 1246 if self.instrumentsFile and (show or onlyFiles): 1247 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1248 fH.write(infoText) 1249 1250 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1251 1252 if self.useHTMLReports: 1253 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1254 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1255 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1256 1257 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1258 1259 return infoText 1260 1261 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1262 """ 1263 This method search and show information about instruments by part of its ticker, FIGI or name. 1264 If `searchResultsFile` string is not empty then also save information to this file. 1265 1266 :param pattern: string with part of ticker, FIGI or instrument's name. 1267 :param show: if `True` then print results to console, if `False` — return list of result only. 1268 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1269 :return: list of dictionaries with all found instruments. 1270 """ 1271 if not self.iList: 1272 self.iList = self.Listing() 1273 1274 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1275 compiledPattern = re.compile(pattern, re.IGNORECASE) 1276 1277 for iType in self.iList: 1278 for instrument in self.iList[iType].values(): 1279 searchResult = compiledPattern.search(" ".join( 1280 [instrument["ticker"], instrument["figi"], instrument["name"]] 1281 )) 1282 1283 if searchResult: 1284 searchResults[iType][instrument["ticker"]] = instrument 1285 1286 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1287 info = [ 1288 "# Search results\n\n", 1289 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1290 "* **Search pattern:** [{}]\n".format(pattern), 1291 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1292 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1293 ] 1294 infoShort = info[:] 1295 1296 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1297 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1298 skippedLine = "| ... | ... | ... | ... |\n" 1299 1300 if resultsLen == 0: 1301 info.append("\nNo results\n") 1302 infoShort.append("\nNo results\n") 1303 uLogger.warning("No results. Try changing your search pattern.") 1304 1305 else: 1306 for iType in searchResults: 1307 iTypeValuesCount = len(searchResults[iType].values()) 1308 if iTypeValuesCount > 0: 1309 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1310 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1311 1312 for instrument in searchResults[iType].values(): 1313 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1314 instrument["type"], 1315 instrument["ticker"], 1316 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1317 instrument["figi"], 1318 )) 1319 1320 if iTypeValuesCount <= 5: 1321 infoShort.extend(info[-iTypeValuesCount:]) 1322 1323 else: 1324 infoShort.extend(info[-5:]) 1325 infoShort.append(skippedLine) 1326 1327 infoText = "".join(info) 1328 infoTextShort = "".join(infoShort) 1329 1330 if show and not onlyFiles: 1331 uLogger.info(infoTextShort) 1332 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1333 1334 if self.searchResultsFile and (show or onlyFiles): 1335 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1336 fH.write(infoText) 1337 1338 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1339 1340 if self.useHTMLReports: 1341 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1342 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1343 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1344 1345 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1346 1347 return searchResults 1348 1349 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1350 """ 1351 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1352 1353 :param instruments: list of strings with tickers or FIGIs. 1354 :return: list with unique instrument FIGIs only. 1355 """ 1356 requestedInstruments = [] 1357 for iName in instruments: 1358 if iName not in self.aliases.keys(): 1359 if iName not in requestedInstruments: 1360 requestedInstruments.append(iName) 1361 1362 else: 1363 if iName not in requestedInstruments: 1364 if self.aliases[iName] not in requestedInstruments: 1365 requestedInstruments.append(self.aliases[iName]) 1366 1367 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1368 1369 onlyUniqueFIGIs = [] 1370 for iName in requestedInstruments: 1371 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1372 continue 1373 1374 self._ticker = iName 1375 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1376 1377 if not iData: 1378 self._ticker = "" 1379 self._figi = iName 1380 1381 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1382 1383 if not iData: 1384 self._figi = "" 1385 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1386 1387 if iData and iData["figi"] not in onlyUniqueFIGIs: 1388 onlyUniqueFIGIs.append(iData["figi"]) 1389 1390 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1391 1392 return onlyUniqueFIGIs 1393 1394 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1395 """ 1396 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1397 1398 See limits: https://tinkoff.github.io/investAPI/limits/ 1399 1400 If `pricesFile` string is not empty then also save information to this file. 1401 1402 :param instruments: list of strings with tickers or FIGIs. 1403 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1404 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1405 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1406 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1407 """ 1408 if instruments is None or not instruments: 1409 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1410 raise Exception("Ticker or FIGI required") 1411 1412 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1413 1414 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1415 1416 iList = [] # trying to get info and current prices about all unique instruments: 1417 for self._figi in onlyUniqueFIGIs: 1418 iData = self.SearchByFIGI(requestPrice=True, show=False) 1419 iList.append(iData) 1420 1421 self.ShowListOfPrices(iList, show, onlyFiles) 1422 1423 return iList 1424 1425 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1426 """ 1427 Show table contains current prices of given instruments. 1428 1429 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1430 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1431 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1432 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1433 :return: multilines text in Markdown format as a table contains current prices. 1434 """ 1435 infoText = "" 1436 1437 if show or self.pricesFile or onlyFiles: 1438 info = [ 1439 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1440 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1441 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1442 ] 1443 1444 for item in iList: 1445 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1446 item["ticker"], 1447 item["figi"], 1448 item["type"], 1449 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1450 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1451 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1452 "{} / {}".format( 1453 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1454 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1455 ), 1456 "{} / {}".format( 1457 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1458 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1459 ), 1460 item["currency"], 1461 )) 1462 1463 infoText = "".join(info) 1464 1465 if show and not onlyFiles: 1466 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1467 1468 if self.pricesFile and (show or onlyFiles): 1469 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1470 fH.write(infoText) 1471 1472 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1473 1474 if self.useHTMLReports: 1475 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1476 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1477 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1478 1479 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1480 1481 return infoText 1482 1483 def RequestTradingStatus(self) -> dict: 1484 """ 1485 Requesting trading status for the instrument defined by `figi` variable. 1486 1487 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1488 1489 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1490 1491 :return: dictionary with trading status attributes. Response example: 1492 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1493 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1494 """ 1495 if self._figi is None or not self._figi: 1496 uLogger.error("Variable `figi` must be defined for using this method!") 1497 raise Exception("FIGI required") 1498 1499 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1500 1501 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1502 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1503 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1504 1505 if self.moreDebug: 1506 uLogger.debug("Records about current trading status successfully received") 1507 1508 return tradingStatus 1509 1510 def RequestPortfolio(self) -> dict: 1511 """ 1512 Requesting actual user's portfolio for current `accountId`. 1513 1514 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1515 1516 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1517 1518 :return: dictionary with user's portfolio. 1519 """ 1520 if self.accountId is None or not self.accountId: 1521 uLogger.error("Variable `accountId` must be defined for using this method!") 1522 raise Exception("Account ID required") 1523 1524 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1525 1526 self.body = str({"accountId": self.accountId}) 1527 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1528 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1529 1530 if self.moreDebug: 1531 uLogger.debug("Records about user's portfolio successfully received") 1532 1533 return rawPortfolio 1534 1535 def RequestPositions(self) -> dict: 1536 """ 1537 Requesting open positions by currencies and instruments for current `accountId`. 1538 1539 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1540 1541 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1542 1543 :return: dictionary with open positions by instruments. 1544 """ 1545 if self.accountId is None or not self.accountId: 1546 uLogger.error("Variable `accountId` must be defined for using this method!") 1547 raise Exception("Account ID required") 1548 1549 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1550 1551 self.body = str({"accountId": self.accountId}) 1552 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1553 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1554 1555 if self.moreDebug: 1556 uLogger.debug("Records about current open positions successfully received") 1557 1558 return rawPositions 1559 1560 def RequestPendingOrders(self) -> list: 1561 """ 1562 Requesting current actual pending limit orders for current `accountId`. 1563 1564 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1565 1566 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1567 1568 :return: list of dictionaries with pending limit orders. 1569 """ 1570 if self.accountId is None or not self.accountId: 1571 uLogger.error("Variable `accountId` must be defined for using this method!") 1572 raise Exception("Account ID required") 1573 1574 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1575 1576 self.body = str({"accountId": self.accountId}) 1577 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1578 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1579 1580 if "orders" in rawResponse.keys(): 1581 rawOrders = rawResponse["orders"] 1582 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1583 1584 else: 1585 rawOrders = [] 1586 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1587 1588 return rawOrders 1589 1590 def RequestStopOrders(self) -> list: 1591 """ 1592 Requesting current actual stop orders for current `accountId`. 1593 1594 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1595 1596 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1597 1598 :return: list of dictionaries with stop orders. 1599 """ 1600 if self.accountId is None or not self.accountId: 1601 uLogger.error("Variable `accountId` must be defined for using this method!") 1602 raise Exception("Account ID required") 1603 1604 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1605 1606 self.body = str({"accountId": self.accountId}) 1607 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1608 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1609 1610 if "stopOrders" in rawResponse.keys(): 1611 rawStopOrders = rawResponse["stopOrders"] 1612 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1613 1614 else: 1615 rawStopOrders = [] 1616 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1617 1618 return rawStopOrders 1619 1620 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1621 """ 1622 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1623 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1624 and `overviewBondsCalendarFile` are defined then also save information to file. 1625 1626 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1627 many requests about the state of the portfolio, and then, based on the received data, a large number 1628 of calculation and statistics are collected. 1629 1630 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1631 :param details: how detailed should the information be? 1632 - `full` — shows full available information about portfolio status (by default), 1633 - `positions` — shows only open positions, 1634 - `orders` — shows only sections of open limits and stop orders. 1635 - `digest` — show a short digest of the portfolio status, 1636 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1637 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1638 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1639 :return: dictionary with client's raw portfolio and some statistics. 1640 """ 1641 if self.accountId is None or not self.accountId: 1642 uLogger.error("Variable `accountId` must be defined for using this method!") 1643 raise Exception("Account ID required") 1644 1645 view = { 1646 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1647 "headers": {}, # list of dictionaries, response headers without "positions" section 1648 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1649 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1650 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1651 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1652 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1653 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1654 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1655 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1656 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1657 }, 1658 "stat": { # --- some statistics calculated using "raw" sections: 1659 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1660 "availableRUB": 0., # available rubles (without other currencies) 1661 "blockedRUB": 0., # blocked sum in Russian Rouble 1662 "totalChangesRUB": 0., # changes for all open trades in RUB 1663 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1664 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1665 "sharesCostRUB": 0., # costs of all shares in RUB 1666 "bondsCostRUB": 0., # costs of all bonds in RUB 1667 "etfsCostRUB": 0., # costs of all etfs in RUB 1668 "futuresCostRUB": 0., # costs of all futures in RUB 1669 "Currencies": [], # list of dictionaries of all currencies statistics 1670 "Shares": [], # list of dictionaries of all shares statistics 1671 "Bonds": [], # list of dictionaries of all bonds statistics 1672 "Etfs": [], # list of dictionaries of all etfs statistics 1673 "Futures": [], # list of dictionaries of all futures statistics 1674 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1675 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1676 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1677 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1678 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1679 }, 1680 "analytics": { # --- some analytics of portfolio: 1681 "distrByAssets": {}, # portfolio distribution by assets 1682 "distrByCompanies": {}, # portfolio distribution by companies 1683 "distrBySectors": {}, # portfolio distribution by sectors 1684 "distrByCurrencies": {}, # portfolio distribution by currencies 1685 "distrByCountries": {}, # portfolio distribution by countries 1686 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1687 } 1688 } 1689 1690 details = details.lower() 1691 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1692 if details not in availableDetails: 1693 details = "full" 1694 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1695 1696 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1697 1698 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1699 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1700 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1701 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1702 1703 # save response headers without "positions" section: 1704 for key in portfolioResponse.keys(): 1705 if key != "positions": 1706 view["raw"]["headers"][key] = portfolioResponse[key] 1707 1708 else: 1709 continue 1710 1711 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1712 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1713 for item in portfolioResponse["positions"]: 1714 if item["instrumentType"] == "currency": 1715 self._figi = item["figi"] 1716 if not self._figi and item["ticker"]: 1717 self._ticker = item["ticker"] 1718 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1719 1720 curr = self.SearchByFIGI(requestPrice=False) 1721 1722 # current price of currency in RUB: 1723 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1724 "name": curr["name"], 1725 "currentPrice": NanoToFloat( 1726 item["currentPrice"]["units"], 1727 item["currentPrice"]["nano"] 1728 ), 1729 } 1730 1731 view["raw"]["Currencies"].append(item) 1732 1733 elif item["instrumentType"] == "share": 1734 view["raw"]["Shares"].append(item) 1735 1736 elif item["instrumentType"] == "bond": 1737 view["raw"]["Bonds"].append(item) 1738 1739 elif item["instrumentType"] == "etf": 1740 view["raw"]["Etfs"].append(item) 1741 1742 elif item["instrumentType"] == "futures": 1743 view["raw"]["Futures"].append(item) 1744 1745 else: 1746 continue 1747 1748 # how many volume of currencies (by ISO currency name) are blocked: 1749 for item in view["raw"]["positions"]["blocked"]: 1750 blocked = NanoToFloat(item["units"], item["nano"]) 1751 if blocked > 0: 1752 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1753 1754 # how many volume of instruments (by FIGI) are blocked: 1755 for item in view["raw"]["positions"]["securities"]: 1756 blocked = int(item["blocked"]) 1757 if blocked > 0: 1758 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1759 1760 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1761 1762 if "rub" in allBlocked.keys(): 1763 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1764 1765 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1766 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1767 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1768 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1769 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1770 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1771 view["stat"]["portfolioCostRUB"] = sum([ 1772 view["stat"]["allCurrenciesCostRUB"], 1773 view["stat"]["sharesCostRUB"], 1774 view["stat"]["bondsCostRUB"], 1775 view["stat"]["etfsCostRUB"], 1776 view["stat"]["futuresCostRUB"], 1777 ]) 1778 1779 # --- calculating some portfolio statistics: 1780 byComp = {} # distribution by companies 1781 bySect = {} # distribution by sectors 1782 byCurr = {} # distribution by currencies (include RUB) 1783 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1784 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1785 1786 for item in portfolioResponse["positions"]: 1787 self._figi = item["figi"] 1788 if not self._figi and item["ticker"]: 1789 self._ticker = item["ticker"] 1790 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1791 1792 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1793 1794 if instrument: 1795 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1796 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1797 1798 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1799 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1800 1801 else: 1802 blocked = 0 1803 1804 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1805 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1806 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1807 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1808 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1809 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1810 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1811 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1812 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1813 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1814 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1815 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1816 1817 statData = { 1818 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1819 "ticker": instrument["ticker"], # ticker by FIGI 1820 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1821 "volume": volume, # available volume of instrument 1822 "lots": lots, # volume in lots of instrument 1823 "direction": direction, # direction of an instrument's position: short or long 1824 "blocked": blocked, # blocked volume of currency or instrument 1825 "currentPrice": curPrice, # current instrument's price in basic asset 1826 "average": average, # current average position price 1827 "cost": cost, # current cost of all volume of instrument in basic asset 1828 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1829 "costRUB": costRUB, # cost of instrument in ruble 1830 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1831 "profit": profit, # expected profit at current moment 1832 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1833 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1834 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1835 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1836 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1837 "step": instrument["step"], # minimum price increment 1838 } 1839 1840 # adding distribution by unique countries: 1841 if statData["country"] not in byCountry.keys(): 1842 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1843 1844 else: 1845 byCountry[statData["country"]]["cost"] += costRUB 1846 byCountry[statData["country"]]["percent"] += percentCostRUB 1847 1848 if item["instrumentType"] != "currency": 1849 # adding distribution by unique companies: 1850 if statData["name"]: 1851 if statData["name"] not in byComp.keys(): 1852 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1853 1854 else: 1855 byComp[statData["name"]]["cost"] += costRUB 1856 byComp[statData["name"]]["percent"] += percentCostRUB 1857 1858 # adding distribution by unique sectors: 1859 if statData["sector"] not in bySect.keys(): 1860 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1861 1862 else: 1863 bySect[statData["sector"]]["cost"] += costRUB 1864 bySect[statData["sector"]]["percent"] += percentCostRUB 1865 1866 # adding distribution by unique currencies: 1867 if currency not in byCurr.keys(): 1868 byCurr[currency] = { 1869 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1870 "cost": costRUB, 1871 "percent": percentCostRUB 1872 } 1873 1874 else: 1875 byCurr[currency]["cost"] += costRUB 1876 byCurr[currency]["percent"] += percentCostRUB 1877 1878 # saving statistics for every instrument: 1879 if item["instrumentType"] == "currency": 1880 view["stat"]["Currencies"].append(statData) 1881 1882 # update dict with free funds for trading (total - blocked) by currencies 1883 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1884 view["stat"]["funds"][currency] = { 1885 "total": volume, 1886 "totalCostRUB": costRUB, # total volume cost in rubles 1887 "free": volume - blocked, 1888 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1889 } 1890 1891 elif item["instrumentType"] == "share": 1892 view["stat"]["Shares"].append(statData) 1893 1894 elif item["instrumentType"] == "bond": 1895 view["stat"]["Bonds"].append(statData) 1896 1897 elif item["instrumentType"] == "etf": 1898 view["stat"]["Etfs"].append(statData) 1899 1900 elif item["instrumentType"] == "Futures": 1901 view["stat"]["Futures"].append(statData) 1902 1903 else: 1904 continue 1905 1906 # total changes in Russian Ruble: 1907 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1908 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1909 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1910 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1911 view["stat"]["funds"]["rub"] = { 1912 "total": view["stat"]["availableRUB"], 1913 "totalCostRUB": view["stat"]["availableRUB"], 1914 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1915 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1916 } 1917 1918 # --- pending limit orders sector data: 1919 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1920 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1921 1922 for item in view["raw"]["orders"]: 1923 self._figi = item["figi"] 1924 1925 if item["figi"] not in uniquePendingOrdersFIGIs: 1926 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1927 1928 uniquePendingOrdersFIGIs.append(item["figi"]) 1929 uniquePendingOrders[item["figi"]] = instrument 1930 1931 else: 1932 instrument = uniquePendingOrders[item["figi"]] 1933 1934 if instrument: 1935 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1936 orderType = TKS_ORDER_TYPES[item["orderType"]] 1937 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1938 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1939 1940 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1941 if item["direction"] == "ORDER_DIRECTION_BUY": 1942 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1943 1944 else: 1945 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1946 1947 # requested price for order execution: 1948 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1949 1950 # necessary changes in percent to reach target from current price: 1951 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1952 1953 view["stat"]["orders"].append({ 1954 "orderID": item["orderId"], # orderId number parameter of current order 1955 "figi": item["figi"], # FIGI identification 1956 "ticker": instrument["ticker"], # ticker name by FIGI 1957 "lotsRequested": item["lotsRequested"], # requested lots value 1958 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1959 "currentPrice": lastPrice, # current instrument's price for defined action 1960 "targetPrice": target, # requested price for order execution in base currency 1961 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1962 "percentChanges": changes, # changes in percent to target from current price 1963 "currency": item["currency"], # instrument's currency name 1964 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1965 "type": orderType, # type of order from TKS_ORDER_TYPES 1966 "status": orderState, # order status from TKS_ORDER_STATES 1967 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1968 }) 1969 1970 # --- stop orders sector data: 1971 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1972 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1973 1974 for item in view["raw"]["stopOrders"]: 1975 self._figi = item["figi"] 1976 1977 if item["figi"] not in uniqueStopOrdersFIGIs: 1978 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1979 1980 uniqueStopOrdersFIGIs.append(item["figi"]) 1981 uniqueStopOrders[item["figi"]] = instrument 1982 1983 else: 1984 instrument = uniqueStopOrders[item["figi"]] 1985 1986 if instrument: 1987 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1988 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1989 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1990 1991 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1992 if "expirationTime" in item.keys(): 1993 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1994 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1995 1996 else: 1997 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1998 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1999 2000 # current instrument's price (last sellers order if buy, and last buyers order if sell): 2001 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 2002 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2003 2004 else: 2005 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2006 2007 # requested price when stop-order executed: 2008 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2009 2010 # price for limit-order, set up when stop-order executed: 2011 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2012 2013 # necessary changes in percent to reach target from current price: 2014 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2015 2016 view["stat"]["stopOrders"].append({ 2017 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2018 "figi": item["figi"], # FIGI identification 2019 "ticker": instrument["ticker"], # ticker name by FIGI 2020 "lotsRequested": item["lotsRequested"], # requested lots value 2021 "currentPrice": lastPrice, # current instrument's price for defined action 2022 "targetPrice": target, # requested price for stop-order execution in base currency 2023 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2024 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2025 "percentChanges": changes, # changes in percent to target from current price 2026 "currency": item["currency"], # instrument's currency name 2027 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2028 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2029 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2030 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2031 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2032 }) 2033 2034 # --- calculating data for analytics section: 2035 # portfolio distribution by assets: 2036 view["analytics"]["distrByAssets"] = { 2037 "Ruble": { 2038 "uniques": 1, 2039 "cost": view["stat"]["availableRUB"], 2040 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2041 }, 2042 "Currencies": { 2043 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2044 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2045 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2046 }, 2047 "Shares": { 2048 "uniques": len(view["stat"]["Shares"]), 2049 "cost": view["stat"]["sharesCostRUB"], 2050 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2051 }, 2052 "Bonds": { 2053 "uniques": len(view["stat"]["Bonds"]), 2054 "cost": view["stat"]["bondsCostRUB"], 2055 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2056 }, 2057 "Etfs": { 2058 "uniques": len(view["stat"]["Etfs"]), 2059 "cost": view["stat"]["etfsCostRUB"], 2060 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2061 }, 2062 "Futures": { 2063 "uniques": len(view["stat"]["Futures"]), 2064 "cost": view["stat"]["futuresCostRUB"], 2065 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2066 }, 2067 } 2068 2069 # portfolio distribution by companies: 2070 view["analytics"]["distrByCompanies"]["All money cash"] = { 2071 "ticker": "", 2072 "cost": view["stat"]["allCurrenciesCostRUB"], 2073 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2074 } 2075 view["analytics"]["distrByCompanies"].update(byComp) 2076 2077 # portfolio distribution by sectors: 2078 view["analytics"]["distrBySectors"]["All money cash"] = { 2079 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2080 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2081 } 2082 view["analytics"]["distrBySectors"].update(bySect) 2083 2084 # portfolio distribution by currencies: 2085 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2086 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2087 2088 if self.moreDebug: 2089 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2090 2091 view["analytics"]["distrByCurrencies"].update(byCurr) 2092 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2093 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2094 2095 # portfolio distribution by countries: 2096 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2097 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2098 2099 if self.moreDebug: 2100 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2101 2102 view["analytics"]["distrByCountries"].update(byCountry) 2103 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2104 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2105 2106 # --- Prepare text statistics overview in human-readable: 2107 if show or onlyFiles: 2108 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2109 2110 # Whatever the value `details`, header not changes: 2111 info = [ 2112 "# Client's portfolio\n\n", 2113 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2114 "* **Account ID:** [{}]\n".format(self.accountId), 2115 ] 2116 2117 if details in ["full", "positions", "digest"]: 2118 info.extend([ 2119 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2120 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2121 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2122 view["stat"]["totalChangesRUB"], 2123 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2124 view["stat"]["totalChangesPercentRUB"], 2125 ), 2126 ]) 2127 2128 if details in ["full", "positions"]: 2129 info.extend([ 2130 "## Open positions\n\n", 2131 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2132 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2133 "| **Ruble:** | {:>31} | | | | | |\n".format( 2134 "{:.2f} ({:.2f}) rub".format( 2135 view["stat"]["availableRUB"], 2136 view["stat"]["blockedRUB"], 2137 ) 2138 ) 2139 ]) 2140 2141 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2142 return [ 2143 "| | | | | | | |\n", 2144 "| {:<27} | | | | | {:>19} | |\n".format( 2145 noTradeStr if noTradeStr else typeStr, 2146 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2147 ), 2148 ] 2149 2150 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2151 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2152 "{} [{}]".format(data["ticker"], data["figi"]), 2153 "{:.2f} ({:.2f}) {}".format( 2154 data["volume"], 2155 data["blocked"], 2156 data["currency"], 2157 ) if isCurr else "{:.0f} ({:.0f})".format( 2158 data["volume"], 2159 data["blocked"], 2160 ), 2161 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2162 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2163 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2164 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2165 "{}{:.2f} {} ({}{:.2f}%)".format( 2166 "+" if data["profit"] > 0 else "", 2167 data["profit"], data["baseCurrencyName"], 2168 "+" if data["percentProfit"] > 0 else "", 2169 data["percentProfit"], 2170 ), 2171 ) 2172 2173 # --- Show currencies section: 2174 if view["stat"]["Currencies"]: 2175 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2176 for item in view["stat"]["Currencies"]: 2177 info.append(_InfoStr(item, isCurr=True)) 2178 2179 else: 2180 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2181 2182 # --- Show shares section: 2183 if view["stat"]["Shares"]: 2184 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2185 2186 for item in view["stat"]["Shares"]: 2187 info.append(_InfoStr(item)) 2188 2189 else: 2190 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2191 2192 # --- Show bonds section: 2193 if view["stat"]["Bonds"]: 2194 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2195 2196 for item in view["stat"]["Bonds"]: 2197 info.append(_InfoStr(item)) 2198 2199 else: 2200 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2201 2202 # --- Show etfs section: 2203 if view["stat"]["Etfs"]: 2204 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2205 2206 for item in view["stat"]["Etfs"]: 2207 info.append(_InfoStr(item)) 2208 2209 else: 2210 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2211 2212 # --- Show futures section: 2213 if view["stat"]["Futures"]: 2214 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2215 2216 for item in view["stat"]["Futures"]: 2217 info.append(_InfoStr(item)) 2218 2219 else: 2220 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2221 2222 if details in ["full", "orders"]: 2223 # --- Show pending limit orders section: 2224 if view["stat"]["orders"]: 2225 info.extend([ 2226 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2227 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2228 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2229 ]) 2230 2231 for item in view["stat"]["orders"]: 2232 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2233 "{} [{}]".format(item["ticker"], item["figi"]), 2234 item["orderID"], 2235 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2236 "{} {} ({}{:.2f}%)".format( 2237 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2238 item["baseCurrencyName"], 2239 "+" if item["percentChanges"] > 0 else "", 2240 float(item["percentChanges"]), 2241 ), 2242 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2243 item["action"], 2244 item["type"], 2245 item["date"], 2246 )) 2247 2248 else: 2249 info.append("\n## Total pending limit-orders: [0]\n") 2250 2251 # --- Show stop orders section: 2252 if view["stat"]["stopOrders"]: 2253 info.extend([ 2254 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2255 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2256 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2257 ]) 2258 2259 for item in view["stat"]["stopOrders"]: 2260 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2261 "{} [{}]".format(item["ticker"], item["figi"]), 2262 item["orderID"], 2263 item["lotsRequested"], 2264 "{} {} ({}{:.2f}%)".format( 2265 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2266 item["baseCurrencyName"], 2267 "+" if item["percentChanges"] > 0 else "", 2268 float(item["percentChanges"]), 2269 ), 2270 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2271 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2272 item["action"], 2273 item["type"], 2274 item["expType"], 2275 item["createDate"], 2276 item["expDate"], 2277 )) 2278 2279 else: 2280 info.append("\n## Total stop-orders: [0]\n") 2281 2282 if details in ["full", "analytics"]: 2283 # -- Show analytics section: 2284 if view["stat"]["portfolioCostRUB"] > 0: 2285 info.extend([ 2286 "\n# Analytics\n\n" 2287 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2288 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2289 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2290 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2291 view["stat"]["totalChangesRUB"], 2292 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2293 view["stat"]["totalChangesPercentRUB"], 2294 ), 2295 "\n## Portfolio distribution by assets\n" 2296 "\n| Type | Uniques | Percent | Current cost |\n", 2297 "|------------------------------------|---------|---------|--------------------|\n", 2298 ]) 2299 2300 for key in view["analytics"]["distrByAssets"].keys(): 2301 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2302 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2303 key, 2304 view["analytics"]["distrByAssets"][key]["uniques"], 2305 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2306 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2307 )) 2308 2309 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2310 2311 info.extend([ 2312 "\n## Portfolio distribution by companies\n" 2313 "\n| Company | Percent | Current cost |\n", 2314 aSepLine, 2315 ]) 2316 2317 for company in view["analytics"]["distrByCompanies"].keys(): 2318 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2319 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2320 "{}{}".format( 2321 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2322 company, 2323 ), 2324 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2325 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2326 )) 2327 2328 info.extend([ 2329 "\n## Portfolio distribution by sectors\n" 2330 "\n| Sector | Percent | Current cost |\n", 2331 aSepLine, 2332 ]) 2333 2334 for sector in view["analytics"]["distrBySectors"].keys(): 2335 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2336 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2337 sector, 2338 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2339 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2340 )) 2341 2342 info.extend([ 2343 "\n## Portfolio distribution by currencies\n" 2344 "\n| Instruments currencies | Percent | Current cost |\n", 2345 aSepLine, 2346 ]) 2347 2348 for curr in view["analytics"]["distrByCurrencies"].keys(): 2349 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2350 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2351 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2352 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2353 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2354 )) 2355 2356 info.extend([ 2357 "\n## Portfolio distribution by countries\n" 2358 "\n| Assets by country | Percent | Current cost |\n", 2359 aSepLine, 2360 ]) 2361 2362 for country in view["analytics"]["distrByCountries"].keys(): 2363 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2364 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2365 country, 2366 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2367 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2368 )) 2369 2370 if details in ["full", "calendar"]: 2371 # -- Show bonds payment calendar section: 2372 if view["stat"]["Bonds"]: 2373 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2374 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2375 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2376 2377 else: 2378 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2379 2380 infoText = "".join(info) 2381 2382 if show and not onlyFiles: 2383 uLogger.info(infoText) 2384 2385 if details == "full" and self.overviewFile: 2386 filename = self.overviewFile 2387 2388 elif details == "digest" and self.overviewDigestFile: 2389 filename = self.overviewDigestFile 2390 2391 elif details == "positions" and self.overviewPositionsFile: 2392 filename = self.overviewPositionsFile 2393 2394 elif details == "orders" and self.overviewOrdersFile: 2395 filename = self.overviewOrdersFile 2396 2397 elif details == "analytics" and self.overviewAnalyticsFile: 2398 filename = self.overviewAnalyticsFile 2399 2400 elif details == "calendar" and self.overviewBondsCalendarFile: 2401 filename = self.overviewBondsCalendarFile 2402 2403 else: 2404 filename = "" 2405 2406 if filename and (show or onlyFiles): 2407 with open(filename, "w", encoding="UTF-8") as fH: 2408 fH.write(infoText) 2409 2410 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2411 2412 if self.useHTMLReports: 2413 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2414 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2415 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2416 2417 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2418 2419 return view 2420 2421 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2422 """ 2423 Returns history operations between two given dates for current `accountId`. 2424 If `reportFile` string is not empty then also save human-readable report. 2425 Shows some statistical data of closed positions. 2426 2427 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2428 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2429 :param show: if `True` then also prints all records to the console. 2430 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2431 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2432 :return: original list of dictionaries with history of deals records from API ("operations" key): 2433 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2434 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2435 """ 2436 if self.accountId is None or not self.accountId: 2437 uLogger.error("Variable `accountId` must be defined for using this method!") 2438 raise Exception("Account ID required") 2439 2440 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2441 2442 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2443 2444 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2445 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2446 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2447 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2448 customStat = {} # custom statistics in additional to responseJSON 2449 2450 # --- output report in human-readable format: 2451 if self.reportFile and (show or onlyFiles): 2452 splitLine1 = "| | | | | |\n" # Summary section 2453 splitLine2 = "| | | | | | | | |\n" # Operations section 2454 nextDay = "" 2455 2456 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2457 2458 if len(ops) > 0: 2459 customStat = { 2460 "opsCount": 0, # total operations count 2461 "buyCount": 0, # buy operations 2462 "sellCount": 0, # sell operations 2463 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2464 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2465 "payIn": {"rub": 0.}, # Deposit brokerage account 2466 "payOut": {"rub": 0.}, # Withdrawals 2467 "divs": {"rub": 0.}, # Dividends income 2468 "coupons": {"rub": 0.}, # Coupon's income 2469 "brokerCom": {"rub": 0.}, # Service commissions 2470 "serviceCom": {"rub": 0.}, # Service commissions 2471 "marginCom": {"rub": 0.}, # Margin commissions 2472 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2473 } 2474 2475 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2476 for item in ops: 2477 if item["state"] == "OPERATION_STATE_EXECUTED": 2478 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2479 2480 # count buy operations: 2481 if "_BUY" in item["operationType"]: 2482 customStat["buyCount"] += 1 2483 2484 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2485 customStat["buyTotal"][item["payment"]["currency"]] += payment 2486 2487 else: 2488 customStat["buyTotal"][item["payment"]["currency"]] = payment 2489 2490 # count sell operations: 2491 elif "_SELL" in item["operationType"]: 2492 customStat["sellCount"] += 1 2493 2494 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2495 customStat["sellTotal"][item["payment"]["currency"]] += payment 2496 2497 else: 2498 customStat["sellTotal"][item["payment"]["currency"]] = payment 2499 2500 # count incoming operations: 2501 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2502 if item["payment"]["currency"] in customStat["payIn"].keys(): 2503 customStat["payIn"][item["payment"]["currency"]] += payment 2504 2505 else: 2506 customStat["payIn"][item["payment"]["currency"]] = payment 2507 2508 # count withdrawals operations: 2509 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2510 if item["payment"]["currency"] in customStat["payOut"].keys(): 2511 customStat["payOut"][item["payment"]["currency"]] += payment 2512 2513 else: 2514 customStat["payOut"][item["payment"]["currency"]] = payment 2515 2516 # count dividends income: 2517 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2518 if item["payment"]["currency"] in customStat["divs"].keys(): 2519 customStat["divs"][item["payment"]["currency"]] += payment 2520 2521 else: 2522 customStat["divs"][item["payment"]["currency"]] = payment 2523 2524 # count coupon's income: 2525 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2526 if item["payment"]["currency"] in customStat["coupons"].keys(): 2527 customStat["coupons"][item["payment"]["currency"]] += payment 2528 2529 else: 2530 customStat["coupons"][item["payment"]["currency"]] = payment 2531 2532 # count broker commissions: 2533 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2534 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2535 customStat["brokerCom"][item["payment"]["currency"]] += payment 2536 2537 else: 2538 customStat["brokerCom"][item["payment"]["currency"]] = payment 2539 2540 # count service commissions: 2541 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2542 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2543 customStat["serviceCom"][item["payment"]["currency"]] += payment 2544 2545 else: 2546 customStat["serviceCom"][item["payment"]["currency"]] = payment 2547 2548 # count margin commissions: 2549 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2550 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2551 customStat["marginCom"][item["payment"]["currency"]] += payment 2552 2553 else: 2554 customStat["marginCom"][item["payment"]["currency"]] = payment 2555 2556 # count withholding taxes: 2557 elif "_TAX" in item["operationType"]: 2558 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2559 customStat["allTaxes"][item["payment"]["currency"]] += payment 2560 2561 else: 2562 customStat["allTaxes"][item["payment"]["currency"]] = payment 2563 2564 else: 2565 continue 2566 2567 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2568 2569 # --- view "Actions" lines: 2570 info.extend([ 2571 "| Report sections | | | | |\n", 2572 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2573 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2574 "| | Buy: {:<22} | {:<28} | | |\n".format( 2575 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2576 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2577 ), 2578 "| | Sell: {:<21} | {:<28} | | |\n".format( 2579 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2580 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2581 ), 2582 ]) 2583 2584 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2585 for key in opsKeys: 2586 if key == "rub": 2587 continue 2588 2589 info.extend([ 2590 "| | | {:<28} | | |\n".format( 2591 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2592 ), 2593 "| | | {:<28} | | |\n".format( 2594 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2595 ), 2596 ]) 2597 2598 info.append(splitLine1) 2599 2600 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2601 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2602 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2603 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2604 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2605 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2606 ) 2607 2608 # --- view "Payments" lines: 2609 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2610 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2611 2612 for key in paymentsKeys: 2613 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2614 2615 info.append(splitLine1) 2616 2617 # --- view "Commissions and taxes" lines: 2618 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2619 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2620 2621 for key in comKeys: 2622 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2623 2624 info.extend([ 2625 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2626 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2627 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2628 ]) 2629 2630 else: 2631 info.append("Broker returned no operations during this period\n") 2632 2633 # --- view "Operations" section: 2634 for item in ops: 2635 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2636 continue 2637 2638 else: 2639 self._figi = item["figi"] 2640 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2641 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2642 2643 # group of deals during one day: 2644 if nextDay and item["date"].split("T")[0] != nextDay: 2645 info.append(splitLine2) 2646 nextDay = "" 2647 2648 else: 2649 nextDay = item["date"].split("T")[0] # saving current day for splitting 2650 2651 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2652 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2653 self._figi if self._figi else "—", 2654 instrument["ticker"] if instrument else "—", 2655 instrument["type"] if instrument else "—", 2656 item["quantity"] if int(item["quantity"]) > 0 else "—", 2657 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2658 TKS_OPERATION_STATES[item["state"]], 2659 TKS_OPERATION_TYPES[item["operationType"]], 2660 )) 2661 2662 infoText = "".join(info) 2663 2664 if show and not onlyFiles: 2665 if self.moreDebug: 2666 uLogger.debug("Records about history of a client's operations successfully received") 2667 2668 uLogger.info(infoText) 2669 2670 if self.reportFile and (show or onlyFiles): 2671 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2672 fH.write(infoText) 2673 2674 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2675 2676 if self.useHTMLReports: 2677 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2678 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2679 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2680 2681 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2682 2683 return ops, customStat 2684 2685 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2686 """ 2687 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2688 2689 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2690 Warning! Broker server used ISO UTC time by default. 2691 2692 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2693 Also, `historyFile` used to update history with `onlyMissing` parameter. 2694 2695 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2696 2697 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2698 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2699 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2700 `"hour"`, `"day"`. Default: `"hour"`. 2701 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2702 False by default. Warning! History appends only from last candle to current time 2703 with always update last candle! 2704 :param csvSep: separator if csv-file is used, `,` by default. 2705 :param show: if `True` then also prints Pandas DataFrame to the console. 2706 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2707 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2708 `["date", "time", "open", "high", "low", "close", "volume"]`. 2709 """ 2710 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2711 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2712 history = None # empty pandas object for history 2713 2714 if interval not in TKS_CANDLE_INTERVALS.keys(): 2715 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2716 raise Exception("Incorrect value") 2717 2718 if not (self._ticker or self._figi): 2719 uLogger.error("Ticker or FIGI must be defined!") 2720 raise Exception("Ticker or FIGI required") 2721 2722 if self._ticker and not self._figi: 2723 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2724 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2725 2726 if self._figi and not self._ticker: 2727 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2728 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2729 2730 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2731 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2732 if interval.lower() != "day": 2733 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2734 2735 delta = dtEnd - dtStart # current UTC time minus last time in file 2736 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2737 2738 # calculate history length in candles: 2739 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2740 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2741 length += 1 # to avoid fraction time 2742 2743 # calculate data blocks count: 2744 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2745 2746 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2747 if self.moreDebug: 2748 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2749 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2750 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2751 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2752 2753 tempOld = None # pandas object for old history, if --only-missing key present 2754 lastTime = None # datetime object of last old candle in file 2755 2756 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2757 if self.moreDebug: 2758 uLogger.debug("--only-missing key present, add only last missing candles...") 2759 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2760 2761 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2762 2763 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2764 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2765 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2766 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2767 2768 # get last datetime object from last string in file or minus 1 delta if file is empty: 2769 if len(tempOld) > 0: 2770 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2771 2772 else: 2773 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2774 2775 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2776 2777 responseJSONs = [] # raw history blocks of data 2778 2779 blockEnd = dtEnd 2780 for item in range(blocks): 2781 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2782 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2783 2784 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2785 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2786 )) 2787 2788 if blockStart == blockEnd: 2789 uLogger.debug("Skipped this zero-length block...") 2790 2791 else: 2792 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2793 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2794 self.body = str({ 2795 "figi": self._figi, 2796 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2797 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2798 "interval": TKS_CANDLE_INTERVALS[interval][0] 2799 }) 2800 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2801 2802 if "code" in responseJSON.keys(): 2803 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2804 2805 else: 2806 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2807 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2808 2809 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2810 2811 blockEnd = blockStart 2812 2813 printCount = len(responseJSONs) # candles to show in console 2814 if responseJSONs: 2815 tempHistory = pd.DataFrame( 2816 data={ 2817 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2818 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2819 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2820 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2821 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2822 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2823 "volume": [int(item["volume"]) for item in responseJSONs], 2824 }, 2825 index=range(len(responseJSONs)), 2826 columns=["date", "time", "open", "high", "low", "close", "volume"], 2827 ) 2828 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2829 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2830 2831 # append only newest candles to old history if --only-missing key present: 2832 if onlyMissing and tempOld is not None and lastTime is not None: 2833 index = 0 # find start index in tempHistory data: 2834 2835 for i, item in tempHistory.iterrows(): 2836 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2837 2838 if curTime == lastTime: 2839 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2840 index = i 2841 printCount = index + 1 2842 break 2843 2844 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2845 2846 else: 2847 history = tempHistory # if no `--only-missing` key then load full data from server 2848 2849 if self.moreDebug: 2850 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2851 2852 if history is not None and not history.empty: 2853 if show and not onlyFiles: 2854 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2855 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2856 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2857 )) 2858 2859 else: 2860 uLogger.warning("Received an empty candles history!") 2861 2862 if self.historyFile is not None: 2863 if history is not None and not history.empty: 2864 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2865 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2866 2867 else: 2868 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2869 2870 else: 2871 if self.moreDebug: 2872 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2873 2874 return history 2875 2876 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2877 """ 2878 Load candles history from csv-file and return Pandas DataFrame object. 2879 2880 See also: `History()` and `ShowHistoryChart()` methods. 2881 2882 :param filePath: path to csv-file to open. 2883 """ 2884 loadedHistory = None # init candles data object 2885 2886 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2887 2888 if os.path.exists(filePath): 2889 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2890 2891 tfStr = self.priceModel.FormattedDelta( 2892 self.priceModel.timeframe, 2893 "{days} days {hours}h {minutes}m {seconds}s", 2894 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2895 self.priceModel.timeframe, 2896 "{hours}h {minutes}m {seconds}s", 2897 ) 2898 2899 if loadedHistory is not None and not loadedHistory.empty: 2900 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2901 len(loadedHistory), 2902 tfStr, 2903 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2904 ) 2905 2906 else: 2907 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2908 2909 else: 2910 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2911 2912 return loadedHistory 2913 2914 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2915 """ 2916 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2917 2918 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2919 Default: `index.html` (both for interact and non-interact candlesticks chart). 2920 2921 See also: `History()` and `LoadHistory()` methods. 2922 2923 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2924 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2925 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2926 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2927 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2928 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2929 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2930 """ 2931 if isinstance(candles, str): 2932 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2933 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2934 2935 elif isinstance(candles, pd.DataFrame): 2936 self.priceModel.prices = candles # set candles chain from variable 2937 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2938 2939 if "datetime" not in candles.columns: 2940 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2941 2942 else: 2943 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2944 raise Exception("Incorrect value") 2945 2946 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2947 2948 if interact: 2949 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2950 2951 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2952 2953 else: 2954 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2955 2956 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2957 2958 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2959 2960 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2961 """ 2962 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2963 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2964 2965 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2966 2967 :param operation: string "Buy" or "Sell". 2968 :param lots: volume, integer count of lots >= 1. 2969 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2970 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2971 :param expDate: string "Undefined" by default or local date in future, 2972 it is a string with format `%Y-%m-%d %H:%M:%S`. 2973 :return: JSON with response from broker server. 2974 """ 2975 if self.accountId is None or not self.accountId: 2976 uLogger.error("Variable `accountId` must be defined for using this method!") 2977 raise Exception("Account ID required") 2978 2979 if operation is None or not operation or operation not in ("Buy", "Sell"): 2980 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2981 raise Exception("Incorrect value") 2982 2983 if lots is None or lots < 1: 2984 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2985 lots = 1 2986 2987 if tp is None or tp < 0: 2988 tp = 0 2989 2990 if sl is None or sl < 0: 2991 sl = 0 2992 2993 if expDate is None or not expDate: 2994 expDate = "Undefined" 2995 2996 if not (self._ticker or self._figi): 2997 uLogger.error("Ticker or FIGI must be defined!") 2998 raise Exception("Ticker or FIGI required") 2999 3000 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3001 self._ticker = instrument["ticker"] 3002 self._figi = instrument["figi"] 3003 3004 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 3005 3006 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3007 self.body = str({ 3008 "figi": self._figi, 3009 "quantity": str(lots), 3010 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3011 "accountId": str(self.accountId), 3012 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3013 }) 3014 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3015 3016 if "orderId" in response.keys(): 3017 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3018 operation, response["orderId"], 3019 self._ticker, self._figi, lots, 3020 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3021 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3022 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3023 )) 3024 3025 if tp > 0: 3026 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3027 3028 if sl > 0: 3029 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3030 3031 else: 3032 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3033 3034 return response 3035 3036 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3037 """ 3038 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3039 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3040 3041 See also: `Order()` and `Trade()` docstrings. 3042 3043 :param lots: volume, integer count of lots >= 1. 3044 :param tp: float > 0, take profit price of stop-order. 3045 :param sl: float > 0, stop loss price of stop-order. 3046 :param expDate: it's a local date in future. 3047 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3048 :return: JSON with response from broker server. 3049 """ 3050 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3051 3052 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3053 """ 3054 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3055 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3056 3057 See also: `Order()` and `Trade()` docstrings. 3058 3059 :param lots: volume, integer count of lots >= 1. 3060 :param tp: float > 0, take profit price of stop-order. 3061 :param sl: float > 0, stop loss price of stop-order. 3062 :param expDate: it's a local date in the future. 3063 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3064 :return: JSON with response from broker server. 3065 """ 3066 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3067 3068 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3069 """ 3070 Close position of given instruments. 3071 3072 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3073 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3074 This avoids unnecessary downloading data from the server. 3075 """ 3076 if instruments is None or not instruments: 3077 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3078 raise Exception("Ticker or FIGI required") 3079 3080 if isinstance(instruments, str): 3081 instruments = [instruments] 3082 3083 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3084 if uniqueInstruments: 3085 if portfolio is None or not portfolio: 3086 portfolio = self.Overview(show=False) 3087 3088 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3089 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3090 3091 for self._figi in uniqueInstruments: 3092 if self._figi not in allOpened: 3093 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3094 continue 3095 3096 # search open trade info about instrument by ticker: 3097 instrument = {} 3098 for iType in TKS_INSTRUMENTS: 3099 if instrument: 3100 break 3101 3102 for item in portfolio["stat"][iType]: 3103 if item["figi"] == self._figi: 3104 instrument = item 3105 break 3106 3107 if instrument: 3108 self._ticker = instrument["ticker"] 3109 self._figi = instrument["figi"] 3110 3111 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3112 self._ticker, 3113 self._figi, 3114 int(instrument["volume"]), 3115 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3116 )) 3117 3118 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3119 3120 if tradeLots > 0: 3121 if instrument["blocked"] > 0: 3122 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3123 instrument["blocked"], 3124 self._ticker, 3125 tradeLots, 3126 )) 3127 3128 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3129 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3130 3131 else: 3132 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3133 3134 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3135 """ 3136 Close all positions of given instruments with defined type. 3137 3138 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3139 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3140 This avoids unnecessary downloading data from the server. 3141 """ 3142 if iType not in TKS_INSTRUMENTS: 3143 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3144 3145 else: 3146 if portfolio is None or not portfolio: 3147 portfolio = self.Overview(show=False) 3148 3149 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3150 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3151 3152 if tickers and portfolio: 3153 self.CloseTrades(tickers, portfolio) 3154 3155 else: 3156 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3157 3158 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3159 """ 3160 Universal method to create market or limit orders with all available parameters for current `accountId`. 3161 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3162 3163 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3164 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3165 3166 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3167 then broker immediately open market order as you can do simple --buy or --sell operations! 3168 3169 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3170 When current price will go up or down to target price value then broker opens a limit order. 3171 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3172 3173 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3174 3175 :param operation: string "Buy" or "Sell". 3176 :param orderType: string "Limit" or "Stop". 3177 :param lots: volume, integer count of lots >= 1. 3178 :param targetPrice: target price > 0. This is open trade price for limit order. 3179 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3180 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3181 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3182 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3183 Stop loss order always executed by market price. 3184 :param expDate: string "Undefined" by default or local date in future. 3185 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3186 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3187 A limit order has no expiration date, it lasts until the end of the trading day. 3188 :return: JSON with response from broker server. 3189 """ 3190 if self.accountId is None or not self.accountId: 3191 uLogger.error("Variable `accountId` must be defined for using this method!") 3192 raise Exception("Account ID required") 3193 3194 if operation is None or not operation or operation not in ("Buy", "Sell"): 3195 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3196 raise Exception("Incorrect value") 3197 3198 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3199 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3200 raise Exception("Incorrect value") 3201 3202 if lots is None or lots < 1: 3203 uLogger.error("You must define trade volume > 0: integer count of lots!") 3204 raise Exception("Incorrect value") 3205 3206 if targetPrice is None or targetPrice <= 0: 3207 uLogger.error("Target price for limit-order must be greater than 0!") 3208 raise Exception("Incorrect value") 3209 3210 if limitPrice is None or limitPrice <= 0: 3211 limitPrice = targetPrice 3212 3213 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3214 stopType = "Limit" 3215 3216 if expDate is None or not expDate: 3217 expDate = "Undefined" 3218 3219 if not (self._ticker or self._figi): 3220 uLogger.error("Tocker or FIGI must be defined!") 3221 raise Exception("Ticker or FIGI required") 3222 3223 response = {} 3224 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3225 self._ticker = instrument["ticker"] 3226 self._figi = instrument["figi"] 3227 3228 if orderType == "Limit": 3229 uLogger.debug( 3230 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3231 self._ticker, self._figi, 3232 operation, lots, targetPrice, instrument["currency"], 3233 )) 3234 3235 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3236 self.body = str({ 3237 "figi": self._figi, 3238 "quantity": str(lots), 3239 "price": FloatToNano(targetPrice), 3240 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3241 "accountId": str(self.accountId), 3242 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3243 }) 3244 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3245 3246 if "orderId" in response.keys(): 3247 uLogger.info( 3248 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3249 response["orderId"], self._ticker, self._figi, operation, lots, 3250 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3251 )) 3252 3253 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3254 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3255 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3256 targetPrice, instrument["currency"], 3257 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3258 )) 3259 3260 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3261 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3262 targetPrice, instrument["currency"], 3263 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3264 )) 3265 3266 else: 3267 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3268 3269 if orderType == "Stop": 3270 uLogger.debug( 3271 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3272 self._ticker, self._figi, 3273 operation, lots, 3274 targetPrice, instrument["currency"], 3275 limitPrice, instrument["currency"], 3276 stopType, expDate, 3277 )) 3278 3279 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3280 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3281 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3282 3283 body = { 3284 "figi": self._figi, 3285 "quantity": str(lots), 3286 "price": FloatToNano(limitPrice), 3287 "stopPrice": FloatToNano(targetPrice), 3288 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3289 "accountId": str(self.accountId), 3290 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3291 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3292 } 3293 3294 if expDateUTC: 3295 body["expireDate"] = expDateUTC 3296 3297 self.body = str(body) 3298 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3299 3300 if "stopOrderId" in response.keys(): 3301 uLogger.info( 3302 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3303 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3304 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3305 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3306 TKS_STOP_ORDER_TYPES[stopOrderType], 3307 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3308 )) 3309 3310 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3311 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3312 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3313 targetPrice, instrument["currency"], 3314 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3315 )) 3316 3317 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3318 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3319 targetPrice, instrument["currency"], 3320 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3321 )) 3322 3323 else: 3324 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3325 3326 return response 3327 3328 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3329 """ 3330 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3331 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3332 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3333 See also: `Order()` docstring. 3334 3335 :param lots: volume, integer count of lots >= 1. 3336 :param targetPrice: target price > 0. This is open trade price for limit order. 3337 :return: JSON with response from broker server. 3338 """ 3339 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3340 3341 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3342 """ 3343 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3344 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3345 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3346 target price value then broker opens a limit order. See also: `Order()` docstring. 3347 3348 :param lots: volume, integer count of lots >= 1. 3349 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3350 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3351 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3352 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3353 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3354 :param expDate: string "Undefined" by default or local date in future. 3355 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3356 This date is converting to UTC format for server. 3357 :return: JSON with response from broker server. 3358 """ 3359 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3360 3361 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3362 """ 3363 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3364 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3365 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3366 See also: `Order()` docstring. 3367 3368 :param lots: volume, integer count of lots >= 1. 3369 :param targetPrice: target price > 0. This is open trade price for limit order. 3370 :return: JSON with response from broker server. 3371 """ 3372 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3373 3374 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3375 """ 3376 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3377 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3378 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3379 target price value then broker opens a limit order. See also: `Order()` docstring. 3380 3381 :param lots: volume, integer count of lots >= 1. 3382 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3383 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3384 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3385 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3386 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3387 :param expDate: string "Undefined" by default or local date in future. 3388 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3389 This date is converting to UTC format for server. 3390 :return: JSON with response from broker server. 3391 """ 3392 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3393 3394 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3395 """ 3396 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3397 3398 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3399 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3400 This avoids unnecessary downloading data from the server. 3401 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3402 """ 3403 if self.accountId is None or not self.accountId: 3404 uLogger.error("Variable `accountId` must be defined for using this method!") 3405 raise Exception("Account ID required") 3406 3407 if orderIDs: 3408 if allOrdersIDs is None: 3409 rawOrders = self.RequestPendingOrders() 3410 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3411 3412 if allStopOrdersIDs is None: 3413 rawStopOrders = self.RequestStopOrders() 3414 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3415 3416 for orderID in orderIDs: 3417 idInPendingOrders = orderID in allOrdersIDs 3418 idInStopOrders = orderID in allStopOrdersIDs 3419 3420 if not (idInPendingOrders or idInStopOrders): 3421 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3422 continue 3423 3424 else: 3425 if idInPendingOrders: 3426 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3427 3428 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3429 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3430 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3431 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3432 3433 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3434 if self.moreDebug: 3435 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3436 3437 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3438 3439 else: 3440 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3441 3442 elif idInStopOrders: 3443 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3444 3445 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3446 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3447 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3448 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3449 3450 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3451 if self.moreDebug: 3452 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3453 3454 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3455 3456 else: 3457 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3458 3459 else: 3460 continue 3461 3462 def CloseAllOrders(self) -> None: 3463 """ 3464 Gets a list of open pending and stop orders and cancel it all. 3465 """ 3466 rawOrders = self.RequestPendingOrders() 3467 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3468 lenOrders = len(allOrdersIDs) 3469 3470 rawStopOrders = self.RequestStopOrders() 3471 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3472 lenSOrders = len(allStopOrdersIDs) 3473 3474 if lenOrders > 0 or lenSOrders > 0: 3475 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3476 3477 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3478 3479 else: 3480 uLogger.info("Orders not found, nothing to cancel.") 3481 3482 def CloseAll(self, *args) -> None: 3483 """ 3484 Close all available (not blocked) opened trades and orders. 3485 3486 Also, you can select one or more keywords case-insensitive: 3487 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3488 3489 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3490 """ 3491 overview = self.Overview(show=False) # get all open trades info 3492 3493 if len(args) == 0: 3494 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3495 self.CloseAllOrders() # close all pending and stop orders 3496 3497 for iType in TKS_INSTRUMENTS: 3498 if iType != "Currencies": 3499 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3500 3501 else: 3502 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3503 lowerArgs = [x.lower() for x in args] 3504 3505 if "orders" in lowerArgs: 3506 self.CloseAllOrders() # close all pending and stop orders 3507 3508 for iType in TKS_INSTRUMENTS: 3509 if iType.lower() in lowerArgs and iType != "Currencies": 3510 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3511 3512 def CloseAllByTicker(self, instrument: str) -> None: 3513 """ 3514 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3515 3516 This method searches opened trade and orders of instrument throw all portfolio and then use 3517 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3518 3519 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3520 3521 :param instrument: string with ticker. 3522 """ 3523 if instrument is None or not instrument: 3524 uLogger.error("Ticker name must be defined for using this method!") 3525 raise Exception("Ticker required") 3526 3527 overview = self.Overview(show=False) # get user portfolio with all open trades info 3528 3529 self._ticker = instrument # try to set instrument as ticker 3530 self._figi = "" 3531 3532 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3533 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3534 3535 if limitAll and self.IsInLimitOrders(portfolio=overview): 3536 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3537 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3538 3539 if stopAll and self.IsInStopOrders(portfolio=overview): 3540 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3541 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3542 3543 if self.IsInPortfolio(portfolio=overview): 3544 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3545 self.CloseTrades(instruments=[instrument], portfolio=overview) 3546 3547 def CloseAllByFIGI(self, instrument: str) -> None: 3548 """ 3549 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3550 3551 This method searches opened trade and orders of instrument throw all portfolio and then use 3552 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3553 3554 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3555 3556 :param instrument: string with FIGI id. 3557 """ 3558 if instrument is None or not instrument: 3559 uLogger.error("FIGI id must be defined for using this method!") 3560 raise Exception("FIGI required") 3561 3562 overview = self.Overview(show=False) # get user portfolio with all open trades info 3563 3564 self._ticker = "" 3565 self._figi = instrument # try to set instrument as FIGI id 3566 3567 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3568 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3569 3570 if limitAll and self.IsInLimitOrders(portfolio=overview): 3571 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3572 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3573 3574 if stopAll and self.IsInStopOrders(portfolio=overview): 3575 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3576 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3577 3578 if self.IsInPortfolio(portfolio=overview): 3579 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3580 self.CloseTrades(instruments=[instrument], portfolio=overview) 3581 3582 @staticmethod 3583 def ParseOrderParameters(operation, **inputParameters): 3584 """ 3585 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3586 3587 :param operation: string "Buy" or "Sell". 3588 :param inputParameters: this is dict of strings that looks like this 3589 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3590 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3591 "prices" key: one or more prices to open limit-orders 3592 Counts of values in lots and prices lists must be equals! 3593 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3594 """ 3595 # TODO: update order grid work with api v2 3596 pass 3597 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3598 # 3599 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3600 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3601 # raise Exception("Incorrect value") 3602 # 3603 # if "l" in inputParameters.keys(): 3604 # inputParameters["lots"] = inputParameters.pop("l") 3605 # 3606 # if "p" in inputParameters.keys(): 3607 # inputParameters["prices"] = inputParameters.pop("p") 3608 # 3609 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3610 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3611 # raise Exception("Incorrect value") 3612 # 3613 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3614 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3615 # 3616 # if len(lots) != len(prices): 3617 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3618 # raise Exception("Incorrect value") 3619 # 3620 # uLogger.debug("Extracted parameters for orders:") 3621 # uLogger.debug("lots = {}".format(lots)) 3622 # uLogger.debug("prices = {}".format(prices)) 3623 # 3624 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3625 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3626 # uLogger.debug("Order parameters: {}".format(result)) 3627 # 3628 # return result 3629 3630 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3631 """ 3632 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3633 3634 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3635 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3636 """ 3637 result = False 3638 msg = "Instrument not defined!" 3639 3640 if portfolio is None or not portfolio: 3641 portfolio = self.Overview(show=False) 3642 3643 if self._ticker: 3644 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3645 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3646 3647 for iType in TKS_INSTRUMENTS: 3648 for instrument in portfolio["stat"][iType]: 3649 if instrument["ticker"] == self._ticker: 3650 result = True 3651 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3652 break 3653 3654 elif self._figi: 3655 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3656 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3657 3658 for iType in TKS_INSTRUMENTS: 3659 for instrument in portfolio["stat"][iType]: 3660 if instrument["figi"] == self._figi: 3661 result = True 3662 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3663 break 3664 3665 else: 3666 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3667 3668 uLogger.debug(msg) 3669 3670 return result 3671 3672 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3673 """ 3674 Returns instrument from the user's portfolio if it presents there. 3675 Instrument must be defined by `ticker` (highly priority) or `figi`. 3676 3677 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3678 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3679 """ 3680 result = None 3681 msg = "Instrument not defined!" 3682 3683 if portfolio is None or not portfolio: 3684 portfolio = self.Overview(show=False) 3685 3686 if self._ticker: 3687 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3688 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3689 3690 for iType in TKS_INSTRUMENTS: 3691 for instrument in portfolio["stat"][iType]: 3692 if instrument["ticker"] == self._ticker: 3693 result = instrument 3694 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3695 break 3696 3697 elif self._figi: 3698 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3699 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3700 3701 for iType in TKS_INSTRUMENTS: 3702 for instrument in portfolio["stat"][iType]: 3703 if instrument["figi"] == self._figi: 3704 result = instrument 3705 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3706 break 3707 3708 else: 3709 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3710 3711 uLogger.debug(msg) 3712 3713 return result 3714 3715 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3716 """ 3717 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3718 3719 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3720 3721 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3722 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3723 """ 3724 result = False 3725 msg = "Instrument not defined!" 3726 3727 if portfolio is None or not portfolio: 3728 portfolio = self.Overview(show=False) 3729 3730 if self._ticker: 3731 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3732 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3733 3734 for instrument in portfolio["stat"]["orders"]: 3735 if instrument["ticker"] == self._ticker: 3736 result = True 3737 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3738 break 3739 3740 elif self._figi: 3741 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3742 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3743 3744 for instrument in portfolio["stat"]["orders"]: 3745 if instrument["figi"] == self._figi: 3746 result = True 3747 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3748 break 3749 3750 else: 3751 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3752 3753 uLogger.debug(msg) 3754 3755 return result 3756 3757 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3758 """ 3759 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3760 Instrument must be defined by `ticker` (highly priority) or `figi`. 3761 3762 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3763 3764 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3765 :return: list with `orderID`s of limit orders. 3766 """ 3767 result = [] 3768 msg = "Instrument not defined!" 3769 3770 if portfolio is None or not portfolio: 3771 portfolio = self.Overview(show=False) 3772 3773 if self._ticker: 3774 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3775 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3776 3777 for instrument in portfolio["stat"]["orders"]: 3778 if instrument["ticker"] == self._ticker: 3779 result.append(instrument["orderID"]) 3780 3781 if result: 3782 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3783 3784 elif self._figi: 3785 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3786 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3787 3788 for instrument in portfolio["stat"]["orders"]: 3789 if instrument["figi"] == self._figi: 3790 result.append(instrument["orderID"]) 3791 3792 if result: 3793 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3794 3795 else: 3796 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3797 3798 uLogger.debug(msg) 3799 3800 return result 3801 3802 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3803 """ 3804 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3805 3806 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3807 3808 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3809 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3810 """ 3811 result = False 3812 msg = "Instrument not defined!" 3813 3814 if portfolio is None or not portfolio: 3815 portfolio = self.Overview(show=False) 3816 3817 if self._ticker: 3818 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3819 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3820 3821 for instrument in portfolio["stat"]["stopOrders"]: 3822 if instrument["ticker"] == self._ticker: 3823 result = True 3824 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3825 break 3826 3827 elif self._figi: 3828 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3829 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3830 3831 for instrument in portfolio["stat"]["stopOrders"]: 3832 if instrument["figi"] == self._figi: 3833 result = True 3834 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3835 break 3836 3837 else: 3838 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3839 3840 uLogger.debug(msg) 3841 3842 return result 3843 3844 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3845 """ 3846 Returns list with all `orderID`s of opened stop orders for the instrument. 3847 Instrument must be defined by `ticker` (highly priority) or `figi`. 3848 3849 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3850 3851 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3852 :return: list with `orderID`s of stop orders. 3853 """ 3854 result = [] 3855 msg = "Instrument not defined!" 3856 3857 if portfolio is None or not portfolio: 3858 portfolio = self.Overview(show=False) 3859 3860 if self._ticker: 3861 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3862 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3863 3864 for instrument in portfolio["stat"]["stopOrders"]: 3865 if instrument["ticker"] == self._ticker: 3866 result.append(instrument["orderID"]) 3867 3868 if result: 3869 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3870 3871 elif self._figi: 3872 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3873 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3874 3875 for instrument in portfolio["stat"]["stopOrders"]: 3876 if instrument["figi"] == self._figi: 3877 result.append(instrument["orderID"]) 3878 3879 if result: 3880 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3881 3882 else: 3883 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3884 3885 uLogger.debug(msg) 3886 3887 return result 3888 3889 def RequestLimits(self) -> dict: 3890 """ 3891 Method for obtaining the available funds for withdrawal for current `accountId`. 3892 3893 See also: 3894 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3895 - `OverviewLimits()` method 3896 3897 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3898 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3899 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3900 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3901 """ 3902 if self.accountId is None or not self.accountId: 3903 uLogger.error("Variable `accountId` must be defined for using this method!") 3904 raise Exception("Account ID required") 3905 3906 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3907 3908 self.body = str({"accountId": self.accountId}) 3909 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3910 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3911 3912 if self.moreDebug: 3913 uLogger.debug("Records about available funds for withdrawal successfully received") 3914 3915 return rawLimits 3916 3917 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3918 """ 3919 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3920 3921 See also: `RequestLimits()`. 3922 3923 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3924 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3925 :return: dict with raw parsed data from server and some calculated statistics about it. 3926 """ 3927 if self.accountId is None or not self.accountId: 3928 uLogger.error("Variable `accountId` must be defined for using this method!") 3929 raise Exception("Account ID required") 3930 3931 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3932 3933 view = { 3934 "rawLimits": rawLimits, 3935 "limits": { # parsed data for every currency: 3936 "money": { # this is an array of portfolio currency positions 3937 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3938 }, 3939 "blocked": { # this is an array of blocked currency 3940 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3941 }, 3942 "blockedGuarantee": { # this is locked money under collateral for futures 3943 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3944 }, 3945 }, 3946 } 3947 3948 # --- Prepare text table with limits in human-readable format: 3949 if show or onlyFiles: 3950 info = [ 3951 "# Withdrawal limits\n\n", 3952 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3953 "* **Account ID:** [{}]\n".format(self.accountId), 3954 ] 3955 3956 if view["limits"]["money"]: 3957 info.extend([ 3958 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3959 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3960 ]) 3961 3962 else: 3963 info.append("\nNo withdrawal limits\n") 3964 3965 for curr in view["limits"]["money"].keys(): 3966 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3967 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3968 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3969 3970 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3971 "[{}]".format(curr), 3972 "{:.2f}".format(view["limits"]["money"][curr]), 3973 "{:.2f}".format(availableMoney), 3974 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3975 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3976 ) 3977 3978 if curr == "rub": 3979 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3980 3981 else: 3982 info.append(infoStr) 3983 3984 infoText = "".join(info) 3985 3986 if show and not onlyFiles: 3987 uLogger.info(infoText) 3988 3989 if self.withdrawalLimitsFile and (show or onlyFiles): 3990 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3991 fH.write(infoText) 3992 3993 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3994 3995 if self.useHTMLReports: 3996 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3997 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3998 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3999 4000 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4001 4002 return view 4003 4004 def RequestAccounts(self) -> dict: 4005 """ 4006 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4007 4008 See also: 4009 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4010 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4011 - `OverviewUserInfo()` method 4012 4013 :return: dict with raw data from server that contains accounts info. Example of dict: 4014 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4015 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4016 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4017 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4018 """ 4019 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4020 4021 self.body = str({}) 4022 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4023 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4024 4025 if self.moreDebug: 4026 uLogger.debug("Records about available accounts successfully received") 4027 4028 return rawAccounts 4029 4030 def RequestUserInfo(self) -> dict: 4031 """ 4032 Method for requesting common user's information. 4033 4034 See also: 4035 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4036 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4037 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4038 - `OverviewUserInfo()` method 4039 4040 :return: dict with raw data from server that contains user's information. Example of dict: 4041 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4042 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4043 """ 4044 uLogger.debug("Requesting common user's information. Wait, please...") 4045 4046 self.body = str({}) 4047 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4048 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4049 4050 if self.moreDebug: 4051 uLogger.debug("Records about current user successfully received") 4052 4053 return rawUserInfo 4054 4055 def RequestMarginStatus(self, accountId: str = None) -> dict: 4056 """ 4057 Method for requesting margin calculation for defined account ID. 4058 4059 See also: 4060 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4061 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4062 - `OverviewUserInfo()` method 4063 4064 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4065 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4066 Example of responses: 4067 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4068 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4069 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4070 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4071 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4072 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4073 """ 4074 if accountId is None or not accountId: 4075 if self.accountId is None or not self.accountId: 4076 uLogger.error("Variable `accountId` must be defined for using this method!") 4077 raise Exception("Account ID required") 4078 4079 else: 4080 accountId = self.accountId # use `self.accountId` (main ID) by default 4081 4082 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4083 4084 self.body = str({"accountId": accountId}) 4085 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4086 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4087 4088 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4089 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4090 rawMargin = {} 4091 4092 else: 4093 if self.moreDebug: 4094 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4095 4096 return rawMargin 4097 4098 def RequestTariffLimits(self) -> dict: 4099 """ 4100 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4101 4102 See also: 4103 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4104 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4105 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4106 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4107 - `OverviewUserInfo()` method 4108 4109 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4110 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4111 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4112 """ 4113 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4114 4115 self.body = str({}) 4116 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4117 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4118 4119 if self.moreDebug: 4120 uLogger.debug("Records with limits of current tariff successfully received") 4121 4122 return rawTariffLimits 4123 4124 def RequestBondCoupons(self, iJSON: dict) -> dict: 4125 """ 4126 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4127 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4128 All dates are in UTC timezone. 4129 4130 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4131 Documentation: 4132 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4133 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4134 4135 See also: `ExtendBondsData()`. 4136 4137 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4138 If raw iJSON is not data of bond then server returns an error [400] with message: 4139 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4140 :return: dictionary with bond payment calendar. Response example 4141 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4142 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4143 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4144 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4145 """ 4146 if iJSON["figi"] is None or not iJSON["figi"]: 4147 uLogger.error("FIGI must be defined for using this method!") 4148 raise Exception("FIGI required") 4149 4150 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4151 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4152 4153 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4154 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4155 self._figi, 4156 startDate, 4157 endDate, 4158 )) 4159 4160 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4161 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4162 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4163 4164 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4165 uLogger.warning("Instrument type is not bond!") 4166 4167 else: 4168 if self.moreDebug: 4169 uLogger.debug("Records about bond payment calendar successfully received") 4170 4171 return calendar 4172 4173 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4174 """ 4175 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4176 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4177 coupon yields, current yields and some statistics etc. 4178 4179 WARNING! This is too long operation if a lot of bonds requested from broker server. 4180 4181 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4182 4183 :param instruments: list of strings with tickers or FIGIs. 4184 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4185 for further used by data scientists or stock analytics. 4186 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4187 In XLSX-file and Pandas DataFrame fields mean: 4188 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4189 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4190 """ 4191 if instruments is None or not instruments: 4192 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4193 raise Exception("Ticker or FIGI required") 4194 4195 if isinstance(instruments, str): 4196 instruments = [instruments] 4197 4198 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4199 4200 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4201 4202 iCount = len(uniqueInstruments) 4203 tooLong = iCount >= 20 4204 if tooLong: 4205 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4206 4207 bonds = None 4208 for i, self._figi in enumerate(uniqueInstruments): 4209 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4210 4211 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4212 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4213 rawBond = self.SearchByFIGI(requestPrice=True) 4214 4215 # Widen raw data with UTC current time (iData["actualDateTime"]): 4216 actualDate = datetime.now(tzutc()) 4217 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4218 4219 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4220 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4221 4222 # Replace some values with human-readable: 4223 iData["nominalCurrency"] = iData["nominal"]["currency"] 4224 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4225 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4226 iData["aciCurrency"] = iData["aciValue"]["currency"] 4227 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4228 iData["issueSize"] = int(iData["issueSize"]) 4229 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4230 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4231 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4232 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4233 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4234 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4235 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4236 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4237 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4238 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4239 4240 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4241 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4242 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4243 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4244 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4245 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4246 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4247 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4248 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4249 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4250 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4251 4252 # Widen raw data with calendar data from `rawCalendar` values: 4253 calendarData = [] 4254 if "events" in iData["rawCalendar"].keys(): 4255 for item in iData["rawCalendar"]["events"]: 4256 calendarData.append({ 4257 "couponDate": item["couponDate"], 4258 "couponNumber": int(item["couponNumber"]), 4259 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4260 "payCurrency": item["payOneBond"]["currency"], 4261 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4262 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4263 "couponStartDate": item["couponStartDate"], 4264 "couponEndDate": item["couponEndDate"], 4265 "couponPeriod": item["couponPeriod"], 4266 }) 4267 4268 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4269 if "maturityDate" not in iData.keys(): 4270 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4271 4272 # Widen raw data with Coupon Rate. 4273 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4274 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4275 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4276 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4277 4278 # Widen raw data with Yield to Maturity (YTM) on current date. 4279 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4280 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4281 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4282 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4283 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4284 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4285 4286 iData["calendar"] = calendarData # adds calendar at the end 4287 4288 # Remove not used data: 4289 iData.pop("uid") 4290 iData.pop("positionUid") 4291 iData.pop("currentPrice") 4292 iData.pop("rawCalendar") 4293 4294 colNames = list(iData.keys()) 4295 if bonds is None: 4296 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4297 4298 else: 4299 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4300 4301 else: 4302 uLogger.warning("Instrument is not a bond!") 4303 4304 processed = round(100 * (i + 1) / iCount, 1) 4305 if tooLong and processed % 5 == 0: 4306 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4307 4308 else: 4309 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4310 4311 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4312 4313 # Saving bonds from Pandas DataFrame to XLSX sheet: 4314 if xlsx and self.bondsXLSXFile: 4315 with pd.ExcelWriter( 4316 path=self.bondsXLSXFile, 4317 date_format=TKS_DATE_FORMAT, 4318 datetime_format=TKS_DATE_TIME_FORMAT, 4319 mode="w", 4320 ) as writer: 4321 bonds.to_excel( 4322 writer, 4323 sheet_name="Extended bonds data", 4324 index=True, 4325 encoding="UTF-8", 4326 freeze_panes=(1, 1), 4327 ) # saving as XLSX-file with freeze first row and column as headers 4328 4329 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4330 4331 return bonds 4332 4333 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4334 """ 4335 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4336 4337 WARNING! This is too long operation if a lot of bonds requested from broker server. 4338 4339 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4340 4341 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4342 extended information about bonds: main info, current prices, bond payment calendar, 4343 coupon yields, current yields and some statistics etc. 4344 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4345 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4346 for further used by data scientists or stock analytics. 4347 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4348 """ 4349 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4350 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4351 4352 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4353 4354 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4355 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4356 calendar = None 4357 for bond in extBonds.iterrows(): 4358 for item in bond[1]["calendar"]: 4359 cData = { 4360 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4361 "couponDate": item["couponDate"], 4362 "figi": bond[1]["figi"], 4363 "ticker": bond[1]["ticker"], 4364 "name": bond[1]["name"], 4365 "couponNumber": item["couponNumber"], 4366 "payOneBond": item["payOneBond"], 4367 "payCurrency": item["payCurrency"], 4368 "couponType": item["couponType"], 4369 "couponPeriod": item["couponPeriod"], 4370 "fixDate": item["fixDate"], 4371 "couponStartDate": item["couponStartDate"], 4372 "couponEndDate": item["couponEndDate"], 4373 } 4374 4375 if calendar is None: 4376 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4377 4378 else: 4379 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4380 4381 if calendar is not None: 4382 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4383 4384 # Saving calendar from Pandas DataFrame to XLSX sheet: 4385 if xlsx: 4386 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4387 4388 with pd.ExcelWriter( 4389 path=xlsxCalendarFile, 4390 date_format=TKS_DATE_FORMAT, 4391 datetime_format=TKS_DATE_TIME_FORMAT, 4392 mode="w", 4393 ) as writer: 4394 humanReadable = calendar.copy(deep=True) 4395 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4396 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4397 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4398 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4399 humanReadable.columns = colNames # human-readable column names 4400 4401 humanReadable.to_excel( 4402 writer, 4403 sheet_name="Bond payments calendar", 4404 index=False, 4405 encoding="UTF-8", 4406 freeze_panes=(1, 2), 4407 ) # saving as XLSX-file with freeze first row and column as headers 4408 4409 del humanReadable # release df in memory 4410 4411 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4412 4413 return calendar 4414 4415 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4416 """ 4417 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4418 Also, creates Markdown file with calendar data, `calendar.md` by default. 4419 4420 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4421 4422 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4423 extended information about bonds: main info, current prices, bond payment calendar, 4424 coupon yields, current yields and some statistics etc. 4425 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4426 :param show: if `True` then also printing bonds payment calendar to the console, 4427 otherwise save to file `calendarFile` only. `False` by default. 4428 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4429 :return: multilines text in Markdown format with bonds payment calendar as a table. 4430 """ 4431 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4432 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4433 4434 infoText = "# Bond payments calendar\n\n" 4435 4436 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4437 4438 if not (calendar is None or calendar.empty): 4439 splitLine = "| | | | | | | | | |\n" 4440 4441 info = [ 4442 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4443 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4444 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4445 ] 4446 4447 newMonth = False 4448 notOneBond = calendar["figi"].nunique() > 1 4449 for i, bond in enumerate(calendar.iterrows()): 4450 if newMonth and notOneBond: 4451 info.append(splitLine) 4452 4453 info.append( 4454 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4455 " √" if bond[1]["paid"] else " —", 4456 bond[1]["couponDate"].split("T")[0], 4457 bond[1]["figi"], 4458 bond[1]["ticker"], 4459 bond[1]["couponNumber"], 4460 "{} {}".format( 4461 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4462 bond[1]["payCurrency"], 4463 ), 4464 bond[1]["couponType"], 4465 bond[1]["couponPeriod"], 4466 bond[1]["fixDate"].split("T")[0], 4467 ) 4468 ) 4469 4470 if i < len(calendar.values) - 1: 4471 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4472 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4473 newMonth = False if curDate.month == nextDate.month else True 4474 4475 else: 4476 newMonth = False 4477 4478 infoText += "".join(info) 4479 4480 if show and not onlyFiles: 4481 uLogger.info("{}".format(infoText)) 4482 4483 if self.calendarFile is not None and (show or onlyFiles): 4484 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4485 fH.write(infoText) 4486 4487 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4488 4489 if self.useHTMLReports: 4490 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4491 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4492 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4493 4494 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4495 4496 else: 4497 infoText += "No data\n" 4498 4499 return infoText 4500 4501 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4502 """ 4503 Method for parsing and show simple table with all available user accounts. 4504 4505 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4506 4507 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4508 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4509 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4510 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4511 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4512 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4513 "closed": "—", "access": "Full access" }, ...}}` 4514 """ 4515 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4516 4517 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4518 accounts = { 4519 item["id"]: { 4520 "type": TKS_ACCOUNT_TYPES[item["type"]], 4521 "name": item["name"], 4522 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4523 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4524 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4525 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4526 } for item in rawAccounts["accounts"] 4527 } 4528 4529 # Raw and parsed data with some fields replaced in "stat" section: 4530 view = { 4531 "rawAccounts": rawAccounts, 4532 "stat": accounts, 4533 } 4534 4535 # --- Prepare simple text table with only accounts data in human-readable format: 4536 if show or onlyFiles: 4537 info = [ 4538 "# User accounts\n\n", 4539 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4540 "| Account ID | Type | Status | Name |\n", 4541 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4542 ] 4543 4544 for account in view["stat"].keys(): 4545 info.extend([ 4546 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4547 account, 4548 view["stat"][account]["type"], 4549 view["stat"][account]["status"], 4550 view["stat"][account]["name"], 4551 ) 4552 ]) 4553 4554 infoText = "".join(info) 4555 4556 if show and not onlyFiles: 4557 uLogger.info(infoText) 4558 4559 if self.userAccountsFile and (show or onlyFiles): 4560 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4561 fH.write(infoText) 4562 4563 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4564 4565 if self.useHTMLReports: 4566 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4567 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4568 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4569 4570 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4571 4572 return view 4573 4574 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4575 """ 4576 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4577 4578 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4579 4580 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4581 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4582 :return: dict with raw parsed data from server and some calculated statistics about it. 4583 """ 4584 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4585 tmpTicker = self._ticker 4586 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4587 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4588 self._ticker = tmpTicker 4589 4590 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4591 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4592 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4593 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4594 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4595 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4596 4597 # This is dict with parsed common user data: 4598 userInfo = { 4599 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4600 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4601 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4602 "tariff": rawUserInfo["tariff"], 4603 } 4604 4605 # This is an array of dict with parsed margin statuses for every account IDs: 4606 margins = {} 4607 for accountId in accounts.keys(): 4608 if rawMargins[accountId]: 4609 margins[accountId] = { 4610 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4611 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4612 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4613 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4614 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4615 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4616 "missing": missing["volume"], 4617 } 4618 4619 else: 4620 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4621 4622 unary = {} # unary-connection limits 4623 for item in rawTariffLimits["unaryLimits"]: 4624 if item["limitPerMinute"] in unary.keys(): 4625 unary[item["limitPerMinute"]].extend(item["methods"]) 4626 4627 else: 4628 unary[item["limitPerMinute"]] = item["methods"] 4629 4630 stream = {} # stream-connection limits 4631 for item in rawTariffLimits["streamLimits"]: 4632 if item["limit"] in stream.keys(): 4633 stream[item["limit"]].extend(item["streams"]) 4634 4635 else: 4636 stream[item["limit"]] = item["streams"] 4637 4638 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4639 limits = { 4640 "unary": unary, 4641 "stream": stream, 4642 } 4643 4644 # Raw and parsed data as an output result: 4645 view = { 4646 "rawUserInfo": rawUserInfo, 4647 "rawAccounts": rawAccounts, 4648 "rawMargins": rawMargins, 4649 "rawTariffLimits": rawTariffLimits, 4650 "stat": { 4651 "overview": overview, 4652 "userInfo": userInfo, 4653 "accounts": accounts, 4654 "margins": margins, 4655 "limits": limits, 4656 }, 4657 } 4658 4659 # --- Prepare text table with user information in human-readable format: 4660 if show or onlyFiles: 4661 info = [ 4662 "# Full user information\n\n", 4663 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4664 "## Common information\n\n", 4665 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4666 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4667 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4668 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4669 "\n## User accounts\n\n", 4670 ] 4671 4672 for account in view["stat"]["accounts"].keys(): 4673 info.extend([ 4674 "### ID: [{}]\n\n".format(account), 4675 "| Parameters | Values |\n", 4676 "|----------------------|--------------------------------------------------------------|\n", 4677 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4678 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4679 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4680 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4681 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4682 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4683 ]) 4684 4685 if margins[account]: 4686 info.extend([ 4687 "| Margin status: | Enabled |\n", 4688 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4689 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4690 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4691 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4692 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4693 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4694 ]) 4695 4696 else: 4697 info.append("| Margin status: | Disabled |\n\n") 4698 4699 info.extend([ 4700 "\n## Current user tariff limits\n", 4701 "\n### See also\n", 4702 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4703 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4704 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4705 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4706 "\n### Unary limits\n", 4707 ]) 4708 4709 if unary: 4710 for key, values in sorted(unary.items()): 4711 info.append("\n* Max requests per minute: {}\n".format(key)) 4712 4713 for value in values: 4714 info.append(" - {}\n".format(value)) 4715 4716 else: 4717 info.append("\nNot available\n") 4718 4719 info.append("\n### Stream limits\n") 4720 4721 if stream: 4722 for key, values in sorted(stream.items()): 4723 info.append("\n* Max stream connections: {}\n".format(key)) 4724 4725 for value in values: 4726 info.append(" - {}\n".format(value)) 4727 4728 else: 4729 info.append("\nNot available\n") 4730 4731 infoText = "".join(info) 4732 4733 if show and not onlyFiles: 4734 uLogger.info(infoText) 4735 4736 if self.userInfoFile and (show or onlyFiles): 4737 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4738 fH.write(infoText) 4739 4740 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4741 4742 if self.useHTMLReports: 4743 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4744 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4745 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4746 4747 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4748 4749 return view 4750 4751 4752class Args: 4753 """ 4754 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4755 """ 4756 def __init__(self, **kwargs): 4757 self.__dict__.update(kwargs) 4758 4759 def __getattr__(self, item): 4760 return None 4761 4762 4763def ParseArgs(): 4764 """This function get and parse command line keys.""" 4765 parser = ArgumentParser() # command-line string parser 4766 4767 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4768 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4769 4770 # --- options: 4771 4772 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4773 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4774 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4775 4776 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4777 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4778 4779 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4780 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4781 4782 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4783 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4784 4785 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4786 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4787 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4788 4789 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4790 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4791 4792 # --- commands: 4793 4794 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4795 4796 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4797 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4798 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4799 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4800 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4801 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4802 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4803 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4804 4805 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4806 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4807 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4808 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4809 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4810 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4811 4812 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4813 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4814 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4815 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4816 4817 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4818 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4819 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4820 4821 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4822 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4823 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4824 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4825 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4826 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4827 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4828 4829 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4830 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4831 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4832 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4833 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4834 4835 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4836 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4837 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4838 4839 cmdArgs = parser.parse_args() 4840 return cmdArgs 4841 4842 4843def Main(**kwargs): 4844 """ 4845 Main function for work with TKSBrokerAPI in the console. 4846 4847 See examples: 4848 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4849 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4850 """ 4851 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4852 4853 if args.debug_level: 4854 uLogger.level = 10 # always debug level by default 4855 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4856 4857 exitCode = 0 4858 start = datetime.now(tzutc()) 4859 uLogger.debug("=-" * 50) 4860 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4861 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4862 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4863 )) 4864 4865 # trying to calculate full current version: 4866 buildVersion = __version__ 4867 try: 4868 v = version("tksbrokerapi") 4869 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4870 4871 except Exception: 4872 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4873 4874 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4875 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4876 4877 try: 4878 if args.version: 4879 print("TKSBrokerAPI {}".format(buildVersion)) 4880 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4881 4882 else: 4883 # Init class for trading with Tinkoff Broker: 4884 trader = TinkoffBrokerServer( 4885 token=args.token, 4886 accountId=args.account_id, 4887 useCache=not args.no_cache, 4888 ) 4889 4890 # --- set some options: 4891 4892 if args.more: 4893 trader.moreDebug = True 4894 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4895 4896 if args.html: 4897 trader.useHTMLReports = True 4898 4899 if args.ticker: 4900 ticker = str(args.ticker).upper() # Tickers may be upper case only 4901 4902 if ticker in trader.aliasesKeys: 4903 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4904 4905 else: 4906 trader.ticker = ticker 4907 4908 if args.figi: 4909 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4910 4911 if args.depth is not None: 4912 trader.depth = args.depth 4913 4914 # --- do one command: 4915 4916 if args.list: 4917 if args.output is not None: 4918 trader.instrumentsFile = args.output 4919 4920 trader.ShowInstrumentsInfo(show=True) 4921 4922 elif args.list_xlsx: 4923 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4924 4925 elif args.bonds_xlsx is not None: 4926 if args.output is not None: 4927 trader.bondsXLSXFile = args.output 4928 4929 if len(args.bonds_xlsx) == 0: 4930 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4931 4932 else: 4933 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4934 4935 elif args.search: 4936 if args.output is not None: 4937 trader.searchResultsFile = args.output 4938 4939 trader.SearchInstruments(pattern=args.search[0], show=True) 4940 4941 elif args.info: 4942 if not (args.ticker or args.figi): 4943 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4944 raise Exception("Ticker or FIGI required") 4945 4946 if args.output is not None: 4947 trader.infoFile = args.output 4948 4949 if args.ticker: 4950 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4951 4952 else: 4953 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4954 4955 elif args.calendar is not None: 4956 if args.output is not None: 4957 trader.calendarFile = args.output 4958 4959 if len(args.calendar) == 0: 4960 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4961 4962 else: 4963 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4964 4965 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4966 4967 elif args.price: 4968 if not (args.ticker or args.figi): 4969 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4970 raise Exception("Ticker or FIGI required") 4971 4972 trader.GetCurrentPrices(show=True) 4973 4974 elif args.prices is not None: 4975 if args.output is not None: 4976 trader.pricesFile = args.output 4977 4978 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4979 4980 elif args.overview: 4981 if args.output is not None: 4982 trader.overviewFile = args.output 4983 4984 trader.Overview(show=True, details="full") 4985 4986 elif args.overview_digest: 4987 if args.output is not None: 4988 trader.overviewDigestFile = args.output 4989 4990 trader.Overview(show=True, details="digest") 4991 4992 elif args.overview_positions: 4993 if args.output is not None: 4994 trader.overviewPositionsFile = args.output 4995 4996 trader.Overview(show=True, details="positions") 4997 4998 elif args.overview_orders: 4999 if args.output is not None: 5000 trader.overviewOrdersFile = args.output 5001 5002 trader.Overview(show=True, details="orders") 5003 5004 elif args.overview_analytics: 5005 if args.output is not None: 5006 trader.overviewAnalyticsFile = args.output 5007 5008 trader.Overview(show=True, details="analytics") 5009 5010 elif args.overview_calendar: 5011 if args.output is not None: 5012 trader.overviewAnalyticsFile = args.output 5013 5014 trader.Overview(show=True, details="calendar") 5015 5016 elif args.deals is not None: 5017 if args.output is not None: 5018 trader.reportFile = args.output 5019 5020 if 0 <= len(args.deals) < 3: 5021 trader.Deals( 5022 start=args.deals[0] if len(args.deals) >= 1 else None, 5023 end=args.deals[1] if len(args.deals) == 2 else None, 5024 show=True, # Always show deals report in console 5025 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5026 ) 5027 5028 else: 5029 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5030 raise Exception("Incorrect value") 5031 5032 elif args.history is not None: 5033 if args.output is not None: 5034 trader.historyFile = args.output 5035 5036 if 0 <= len(args.history) < 3: 5037 dataReceived = trader.History( 5038 start=args.history[0] if len(args.history) >= 1 else None, 5039 end=args.history[1] if len(args.history) == 2 else None, 5040 interval="hour" if args.interval is None or not args.interval else args.interval, 5041 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5042 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5043 show=True, # shows all downloaded candles in console 5044 ) 5045 5046 if args.render_chart is not None and dataReceived is not None: 5047 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5048 5049 trader.ShowHistoryChart( 5050 candles=dataReceived, 5051 interact=iChart, 5052 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5053 ) 5054 5055 else: 5056 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5057 raise Exception("Incorrect value") 5058 5059 elif args.load_history is not None: 5060 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5061 5062 if args.render_chart is not None and histData is not None: 5063 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5064 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5065 5066 trader.ShowHistoryChart( 5067 candles=histData, 5068 interact=iChart, 5069 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5070 ) 5071 5072 elif args.trade is not None: 5073 if 1 <= len(args.trade) <= 5: 5074 trader.Trade( 5075 operation=args.trade[0], 5076 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5077 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5078 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5079 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5080 ) 5081 5082 else: 5083 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5084 5085 elif args.buy is not None: 5086 if 0 <= len(args.buy) <= 4: 5087 trader.Buy( 5088 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5089 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5090 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5091 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5092 ) 5093 5094 else: 5095 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5096 5097 elif args.sell is not None: 5098 if 0 <= len(args.sell) <= 4: 5099 trader.Sell( 5100 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5101 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5102 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5103 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5104 ) 5105 5106 else: 5107 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5108 5109 elif args.order: 5110 if 4 <= len(args.order) <= 7: 5111 trader.Order( 5112 operation=args.order[0], 5113 orderType=args.order[1], 5114 lots=int(args.order[2]), 5115 targetPrice=float(args.order[3]), 5116 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5117 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5118 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5119 ) 5120 5121 else: 5122 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5123 5124 elif args.buy_limit: 5125 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5126 5127 elif args.sell_limit: 5128 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5129 5130 elif args.buy_stop: 5131 if 2 <= len(args.buy_stop) <= 7: 5132 trader.BuyStop( 5133 lots=int(args.buy_stop[0]), 5134 targetPrice=float(args.buy_stop[1]), 5135 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5136 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5137 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5138 ) 5139 5140 else: 5141 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5142 5143 elif args.sell_stop: 5144 if 2 <= len(args.sell_stop) <= 7: 5145 trader.SellStop( 5146 lots=int(args.sell_stop[0]), 5147 targetPrice=float(args.sell_stop[1]), 5148 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5149 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5150 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5151 ) 5152 5153 else: 5154 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5155 5156 # elif args.buy_order_grid is not None: 5157 # # update order grid work with api v2 5158 # if len(args.buy_order_grid) == 2: 5159 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5160 # 5161 # for order in orderParams: 5162 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5163 # 5164 # else: 5165 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5166 # 5167 # elif args.sell_order_grid is not None: 5168 # # update order grid work with api v2 5169 # if len(args.sell_order_grid) >= 2: 5170 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5171 # 5172 # for order in orderParams: 5173 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5174 # 5175 # else: 5176 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5177 5178 elif args.close_order is not None: 5179 trader.CloseOrders(args.close_order) # close only one order 5180 5181 elif args.close_orders is not None: 5182 trader.CloseOrders(args.close_orders) # close list of orders 5183 5184 elif args.close_trade: 5185 if not (args.ticker or args.figi): 5186 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5187 raise Exception("Ticker or FIGI required") 5188 5189 if args.ticker: 5190 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5191 5192 else: 5193 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5194 5195 elif args.close_trades is not None: 5196 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5197 5198 elif args.close_all is not None: 5199 if args.ticker: 5200 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5201 5202 elif args.figi: 5203 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5204 5205 else: 5206 trader.CloseAll(*args.close_all) 5207 5208 elif args.limits: 5209 if args.output is not None: 5210 trader.withdrawalLimitsFile = args.output 5211 5212 trader.OverviewLimits(show=True) 5213 5214 elif args.user_info: 5215 if args.output is not None: 5216 trader.userInfoFile = args.output 5217 5218 trader.OverviewUserInfo(show=True) 5219 5220 elif args.account: 5221 if args.output is not None: 5222 trader.userAccountsFile = args.output 5223 5224 trader.OverviewAccounts(show=True) 5225 5226 else: 5227 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5228 raise Exception("There is no command to execute") 5229 5230 except Exception: 5231 trace = tb.format_exc() 5232 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5233 if e in trace: 5234 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5235 break 5236 5237 uLogger.debug(trace) 5238 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5239 exitCode = 255 # an error occurred, must be open a ticket for this issue 5240 5241 finally: 5242 finish = datetime.now(tzutc()) 5243 5244 if exitCode == 0: 5245 if args.more: 5246 uLogger.debug("All operations were finished success (summary code is 0).") 5247 5248 else: 5249 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5250 os.path.abspath(uLog.defaultLogFile), exitCode, 5251 )) 5252 5253 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5254 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5255 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5256 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5257 )) 5258 uLogger.debug("=-" * 50) 5259 5260 if not kwargs: 5261 sys.exit(exitCode) 5262 5263 else: 5264 return exitCode 5265 5266 5267if __name__ == "__main__": 5268 Main()
78class TinkoffBrokerServer: 79 """ 80 This class implements methods to work with Tinkoff broker server. 81 82 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 83 84 About `token`: https://tinkoff.github.io/investAPI/token/ 85 """ 86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self.__lock = Lock() # initialize multiprocessing mutex lock 130 131 self.aliases = TKS_TICKER_ALIASES 132 """Some aliases instead official tickers. 133 134 See also: `TKSEnums.TKS_TICKER_ALIASES` 135 """ 136 137 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 138 139 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 140 141 self._ticker = "" 142 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 143 144 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 145 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 146 147 See also: `SearchByTicker()`, `SearchInstruments()`. 148 """ 149 150 self._figi = "" 151 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 152 153 See also: `SearchByFIGI()`, `SearchInstruments()`. 154 """ 155 156 self.depth = 1 157 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 158 159 See also: `GetCurrentPrices()`. 160 """ 161 162 self.server = r"https://invest-public-api.tinkoff.ru/rest" 163 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 164 165 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 166 """ 167 168 uLogger.debug("Broker API server: {}".format(self.server)) 169 170 self.timeout = 15 171 """Server operations timeout in seconds. Default: `15`. 172 173 See also: `SendAPIRequest()`. 174 """ 175 176 self.headers = { 177 "Content-Type": "application/json", 178 "accept": "application/json", 179 "Authorization": "Bearer {}".format(self.token), 180 "x-app-name": "Tim55667757.TKSBrokerAPI", 181 } 182 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 183 184 See also: `SendAPIRequest()`. 185 """ 186 187 self.body = None 188 """Request body which send to broker server. Default: `None`. 189 190 See also: `SendAPIRequest()`. 191 """ 192 193 self.moreDebug = False 194 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 195 196 self.useHTMLReports = False 197 """ 198 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 199 200 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 201 """ 202 203 self.historyFile = None 204 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 205 206 See also: `History()`. 207 """ 208 209 self.htmlHistoryFile = "index.html" 210 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 211 212 See also: `ShowHistoryChart()`. 213 """ 214 215 self.instrumentsFile = "instruments.md" 216 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 217 218 See also: `ShowInstrumentsInfo()`. 219 """ 220 221 self.searchResultsFile = "search-results.md" 222 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 223 224 See also: `SearchInstruments()`. 225 """ 226 227 self.pricesFile = "prices.md" 228 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 229 230 See also: `GetListOfPrices()`. 231 """ 232 233 self.infoFile = "info.md" 234 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 235 236 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 237 """ 238 239 self.bondsXLSXFile = "ext-bonds.xlsx" 240 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 241 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 242 243 See also: `ExtendBondsData()`. 244 """ 245 246 self.calendarFile = "calendar.md" 247 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 248 249 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 250 251 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 252 """ 253 254 self.overviewFile = "overview.md" 255 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 256 257 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 258 """ 259 260 self.overviewDigestFile = "overview-digest.md" 261 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 262 263 See also: `Overview()` with parameter `details="digest"`. 264 """ 265 266 self.overviewPositionsFile = "overview-positions.md" 267 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 268 269 See also: `Overview()` with parameter `details="positions"`. 270 """ 271 272 self.overviewOrdersFile = "overview-orders.md" 273 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 274 275 See also: `Overview()` with parameter `details="orders"`. 276 """ 277 278 self.overviewAnalyticsFile = "overview-analytics.md" 279 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 280 281 See also: `Overview()` with parameter `details="analytics"`. 282 """ 283 284 self.overviewBondsCalendarFile = "overview-calendar.md" 285 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 286 287 See also: `Overview()` with parameter `details="calendar"`. 288 """ 289 290 self.reportFile = "deals.md" 291 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 292 293 See also: `Deals()`. 294 """ 295 296 self.withdrawalLimitsFile = "limits.md" 297 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 298 299 See also: `OverviewLimits()` and `RequestLimits()`. 300 """ 301 302 self.userInfoFile = "user-info.md" 303 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 304 305 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 306 """ 307 308 self.userAccountsFile = "accounts.md" 309 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 310 311 See also: `OverviewAccounts()`, `RequestAccounts()`. 312 """ 313 314 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 315 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 316 317 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 318 319 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 320 """ 321 322 self.iList = None # init iList for raw instruments data 323 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 324 325 See also: `Listing()`, `DumpInstruments()`. 326 """ 327 328 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 329 if useCache: 330 if os.path.exists(self.iListDumpFile): 331 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 332 curTime = datetime.now(tzutc()) 333 334 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 335 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 336 337 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 338 339 else: 340 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 341 342 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 343 os.path.abspath(self.iListDumpFile), 344 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 345 )) 346 347 else: 348 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 349 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 350 351 else: 352 self.iList = self.Listing() # request new raw instruments data from broker server 353 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 354 355 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 356 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 357 358 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 359 """ 360 361 @property 362 def ticker(self) -> str: 363 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 364 365 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 366 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 367 368 See also: `SearchByTicker()`, `SearchInstruments()`. 369 """ 370 return self._ticker 371 372 @ticker.setter 373 def ticker(self, value): 374 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 375 376 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 377 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 378 379 See also: `SearchByTicker()`, `SearchInstruments()`. 380 """ 381 self._ticker = str(value).upper() # Tickers may be upper case only 382 383 @property 384 def figi(self) -> str: 385 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 386 387 See also: `SearchByFIGI()`, `SearchInstruments()`. 388 """ 389 return self._figi 390 391 @figi.setter 392 def figi(self, value): 393 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 394 395 See also: `SearchByFIGI()`, `SearchInstruments()`. 396 """ 397 self._figi = str(value).upper() # FIGI may be upper case only 398 399 def _ParseJSON(self, rawData="{}") -> dict: 400 """ 401 Parse JSON from response string. 402 403 :param rawData: this is a string with JSON-formatted text. 404 :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`. 405 """ 406 try: 407 responseJSON = json.loads(rawData) if rawData else {} 408 409 if self.moreDebug: 410 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 411 412 return responseJSON 413 414 except Exception as e: 415 uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e)) 416 417 return {} 418 419 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 420 """ 421 Send GET or POST request to broker server and receive JSON object. 422 423 self.header: must be defining with dictionary of headers. 424 self.body: if define then used as request body. None by default. 425 self.timeout: global request timeout, 15 seconds by default. 426 :param url: url with REST request. 427 :param reqType: send "GET" or "POST" request. "GET" by default. 428 :param retry: how many times retry after first request if an 5xx server errors occurred. 429 :param pause: sleep time in seconds between retries. 430 :return: response JSON (dictionary) from broker. 431 """ 432 if reqType.upper() not in ("GET", "POST"): 433 uLogger.error("You can define request type: `GET` or `POST`!") 434 raise Exception("Incorrect value") 435 436 if self.moreDebug: 437 uLogger.debug("Request parameters:") 438 uLogger.debug(" - REST API URL: {}".format(url)) 439 uLogger.debug(" - request type: {}".format(reqType)) 440 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 441 uLogger.debug(" - body:\n{}".format(self.body)) 442 443 # fast hack to avoid all operations with some tickers/FIGI 444 responseJSON = {} 445 oK = True 446 for item in self.exclude: 447 if item in url: 448 if self.moreDebug: 449 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 450 451 oK = False 452 break 453 454 if oK: 455 with self.__lock: # acquire the mutex lock 456 counter = 0 457 response = None 458 errMsg = "" 459 460 while not response and counter <= retry: 461 if reqType == "GET": 462 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 463 464 if reqType == "POST": 465 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 466 467 if self.moreDebug: 468 uLogger.debug("Response:") 469 uLogger.debug(" - status code: {}".format(response.status_code)) 470 uLogger.debug(" - reason: {}".format(response.reason)) 471 uLogger.debug(" - body length: {}".format(len(response.text))) 472 uLogger.debug(" - headers:\n{}".format(response.headers)) 473 474 # Server returns some headers: 475 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 476 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 477 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 478 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 479 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 480 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 481 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 482 sleep(rateLimitWait) 483 484 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 485 if 400 <= response.status_code < 500: 486 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 487 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 488 489 if "code" in response.text and "message" in response.text: 490 msgDict = self._ParseJSON(rawData=response.text) 491 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 492 493 counter = retry + 1 # do not retry for 4xx errors 494 495 if 500 <= response.status_code < 600: 496 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 497 uLogger.debug(" - not oK, {}".format(errMsg)) 498 499 if "code" in response.text and "message" in response.text: 500 errMsgDict = self._ParseJSON(rawData=response.text) 501 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 502 503 counter += 1 504 505 if counter <= retry: 506 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 507 sleep(pause) 508 509 responseJSON = self._ParseJSON(rawData=response.text) 510 511 if errMsg: 512 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 513 uLogger.error(" - not oK, {}".format(errMsg)) 514 515 return responseJSON 516 517 def _IUpdater(self, iType: str) -> tuple: 518 """ 519 Request instrument by type from server. See available API methods for instruments: 520 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 521 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 522 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 523 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 524 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 525 526 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 527 :return: tuple with iType name and list of available instruments of current type for defined user token. 528 """ 529 result = [] 530 531 if iType in TKS_INSTRUMENTS: 532 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 533 534 # all instruments have the same body in API v2 requests: 535 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 536 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 537 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 538 539 return iType, result 540 541 def _IWrapper(self, kwargs): 542 """ 543 Wrapper runs instrument's update method `_IUpdater()`. 544 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 545 """ 546 return self._IUpdater(**kwargs) 547 548 def Listing(self) -> dict: 549 """ 550 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 551 552 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 553 """ 554 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 555 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 556 557 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 558 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 559 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 560 561 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 562 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 563 poolUpdater.close() # close the thread pool 564 poolUpdater.join() # wait a moment until all data returns from threads 565 566 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 567 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 568 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 569 570 # calculate minimum price increment (step) for all instruments and set up instrument's type: 571 for iType in iList.keys(): 572 for ticker in iList[iType]: 573 iList[iType][ticker]["type"] = iType 574 575 if "minPriceIncrement" in iList[iType][ticker].keys(): 576 iList[iType][ticker]["step"] = NanoToFloat( 577 iList[iType][ticker]["minPriceIncrement"]["units"], 578 iList[iType][ticker]["minPriceIncrement"]["nano"], 579 ) 580 581 else: 582 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 583 584 return iList 585 586 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 587 """ 588 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 589 590 See also: `DumpInstruments()`, `Listing()`. 591 592 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 593 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 594 """ 595 if self.iListDumpFile is None or not self.iListDumpFile: 596 uLogger.error("Output name of dump file must be defined!") 597 raise Exception("Filename required") 598 599 if not self.iList or forceUpdate: 600 self.iList = self.Listing() 601 602 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 603 604 # Save as XLSX with separated sheets for every type of instruments: 605 with pd.ExcelWriter( 606 path=xlsxDumpFile, 607 date_format=TKS_DATE_FORMAT, 608 datetime_format=TKS_DATE_TIME_FORMAT, 609 mode="w", 610 ) as writer: 611 for iType in TKS_INSTRUMENTS: 612 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 613 df = df[sorted(df)] # sorted by column names 614 df = df.applymap( 615 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 616 na_action="ignore", 617 ) # converting numbers from nano-type to float in every cell 618 df.to_excel( 619 writer, 620 sheet_name=iType, 621 encoding="UTF-8", 622 freeze_panes=(1, 1), 623 ) # saving as XLSX-file with freeze first row and column as headers 624 625 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 626 627 def DumpInstruments(self, forceUpdate: bool = True) -> str: 628 """ 629 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 630 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 631 632 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 633 634 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 635 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 636 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 637 """ 638 if self.iListDumpFile is None or not self.iListDumpFile: 639 uLogger.error("Output name of dump file must be defined!") 640 raise Exception("Filename required") 641 642 if not self.iList or forceUpdate: 643 self.iList = self.Listing() 644 645 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 646 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 647 fH.write(jsonDump) 648 649 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 650 651 return jsonDump 652 653 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 654 """ 655 Show information about one instrument defined by json data and prints it in Markdown format. 656 657 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 658 659 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 660 :param show: if `True` then also printing information about instrument and its current price. 661 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 662 :return: multilines text in Markdown format with information about one instrument. 663 """ 664 splitLine = "| | |\n" 665 infoText = "" 666 667 if iJSON is not None and iJSON and isinstance(iJSON, dict): 668 info = [ 669 "# Main information\n\n", 670 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 671 "| Parameters | Values |\n", 672 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 673 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 674 "| Full name: | {:<54} |\n".format(iJSON["name"]), 675 ] 676 677 if "sector" in iJSON.keys() and iJSON["sector"]: 678 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 679 680 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 681 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 682 683 info.extend([ 684 splitLine, 685 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 686 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 687 ]) 688 689 if "isin" in iJSON.keys() and iJSON["isin"]: 690 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 691 692 if "classCode" in iJSON.keys(): 693 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 694 695 info.extend([ 696 splitLine, 697 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 698 splitLine, 699 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 700 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 701 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 702 ]) 703 704 if iJSON["figi"]: 705 self._figi = iJSON["figi"] 706 iJSON = iJSON | self.RequestTradingStatus() 707 708 info.extend([ 709 splitLine, 710 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 711 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 712 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 713 ]) 714 715 info.append(splitLine) 716 717 if "type" in iJSON.keys() and iJSON["type"]: 718 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 719 720 if "shareType" in iJSON.keys() and iJSON["shareType"]: 721 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 722 723 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 724 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 725 726 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 727 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 728 729 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 730 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 731 732 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 733 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 734 735 if "focusType" in iJSON.keys() and iJSON["focusType"]: 736 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 737 738 if "assetType" in iJSON.keys() and iJSON["assetType"]: 739 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 740 741 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 742 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 743 744 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 745 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 746 747 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 748 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 749 750 if "currency" in iJSON.keys(): 751 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 752 753 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 754 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 755 756 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 757 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 758 759 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 760 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 761 762 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 763 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 764 765 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 766 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 767 768 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 769 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 770 771 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 772 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 773 774 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 775 info.append("| Perpetual bond: | Yes |\n") 776 777 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 778 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 779 780 iExt = None 781 if iJSON["type"] == "Bonds": 782 info.extend([ 783 splitLine, 784 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 785 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 786 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 787 iJSON["nominal"]["currency"], 788 )), 789 ]) 790 791 if "floatingCouponFlag" in iJSON.keys(): 792 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 793 794 if "amortizationFlag" in iJSON.keys(): 795 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 796 797 info.append(splitLine) 798 799 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 800 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 801 802 if iJSON["figi"]: 803 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 804 805 info.extend([ 806 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 807 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 808 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 809 ]) 810 811 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 812 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 813 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 814 iJSON["aciValue"]["currency"] 815 ))) 816 817 if "currentPrice" in iJSON.keys(): 818 info.append(splitLine) 819 820 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 821 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 822 823 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 824 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 825 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 826 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 827 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 828 829 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 830 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 831 832 info.extend([ 833 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 834 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 835 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 836 )), 837 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 838 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 839 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 840 )), 841 "| Changes between last deal price and last close | {:<54} |\n".format( 842 "{:.2f}%{}".format( 843 iJSON["currentPrice"]["changes"], 844 " ({}{:.2f} {})".format( 845 "+" if bondChangesDelta > 0 else "", 846 bondChangesDelta, 847 aciCurrency 848 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 849 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 850 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 851 currency 852 ), 853 ) 854 ), 855 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 856 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 857 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 858 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 859 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 860 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 861 )), 862 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 863 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 864 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 865 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 866 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 867 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 868 )), 869 ]) 870 871 if "lot" in iJSON.keys(): 872 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 873 874 if "step" in iJSON.keys() and iJSON["step"] != 0: 875 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 876 877 # Add bond payment calendar: 878 if iJSON["type"] == "Bonds": 879 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 880 info.extend(["\n#", strCalendar]) 881 882 infoText += "".join(info) 883 884 if show and not onlyFiles: 885 uLogger.info("{}".format(infoText)) 886 887 if self.infoFile is not None and (show or onlyFiles): 888 with open(self.infoFile, "w", encoding="UTF-8") as fH: 889 fH.write(infoText) 890 891 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 892 893 if self.useHTMLReports: 894 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 895 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 896 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 897 898 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 899 900 return infoText 901 902 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 903 """ 904 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 905 906 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 907 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 908 :return: JSON formatted data with information about instrument. 909 """ 910 tickerJSON = {} 911 if self.moreDebug: 912 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 913 914 if not self._ticker: 915 uLogger.warning("self._ticker variable is not be empty!") 916 917 else: 918 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 919 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 920 raise Exception("Instrument not allowed") 921 922 if not self.iList: 923 self.iList = self.Listing() 924 925 if self._ticker in self.iList["Shares"].keys(): 926 tickerJSON = self.iList["Shares"][self._ticker] 927 if self.moreDebug: 928 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 929 930 elif self._ticker in self.iList["Currencies"].keys(): 931 tickerJSON = self.iList["Currencies"][self._ticker] 932 if self.moreDebug: 933 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 934 935 elif self._ticker in self.iList["Bonds"].keys(): 936 tickerJSON = self.iList["Bonds"][self._ticker] 937 if self.moreDebug: 938 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 939 940 elif self._ticker in self.iList["Etfs"].keys(): 941 tickerJSON = self.iList["Etfs"][self._ticker] 942 if self.moreDebug: 943 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 944 945 elif self._ticker in self.iList["Futures"].keys(): 946 tickerJSON = self.iList["Futures"][self._ticker] 947 if self.moreDebug: 948 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 949 950 if tickerJSON: 951 self._figi = tickerJSON["figi"] 952 953 if requestPrice: 954 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 955 956 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 957 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 958 959 else: 960 tickerJSON["currentPrice"]["changes"] = 0 961 962 if show: 963 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 964 965 else: 966 if show: 967 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 968 969 return tickerJSON 970 971 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 972 """ 973 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 974 975 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 976 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 977 :return: JSON formatted data with information about instrument. 978 """ 979 figiJSON = {} 980 if self.moreDebug: 981 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 982 983 if not self._figi: 984 uLogger.warning("self._figi variable is not be empty!") 985 986 else: 987 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 988 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 989 raise Exception("Instrument not allowed") 990 991 if not self.iList: 992 self.iList = self.Listing() 993 994 for item in self.iList["Shares"].keys(): 995 if self._figi == self.iList["Shares"][item]["figi"]: 996 figiJSON = self.iList["Shares"][item] 997 998 if self.moreDebug: 999 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1000 1001 break 1002 1003 if not figiJSON: 1004 for item in self.iList["Currencies"].keys(): 1005 if self._figi == self.iList["Currencies"][item]["figi"]: 1006 figiJSON = self.iList["Currencies"][item] 1007 1008 if self.moreDebug: 1009 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1010 1011 break 1012 1013 if not figiJSON: 1014 for item in self.iList["Bonds"].keys(): 1015 if self._figi == self.iList["Bonds"][item]["figi"]: 1016 figiJSON = self.iList["Bonds"][item] 1017 1018 if self.moreDebug: 1019 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1020 1021 break 1022 1023 if not figiJSON: 1024 for item in self.iList["Etfs"].keys(): 1025 if self._figi == self.iList["Etfs"][item]["figi"]: 1026 figiJSON = self.iList["Etfs"][item] 1027 1028 if self.moreDebug: 1029 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1030 1031 break 1032 1033 if not figiJSON: 1034 for item in self.iList["Futures"].keys(): 1035 if self._figi == self.iList["Futures"][item]["figi"]: 1036 figiJSON = self.iList["Futures"][item] 1037 1038 if self.moreDebug: 1039 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1040 1041 break 1042 1043 if figiJSON: 1044 self._figi = figiJSON["figi"] 1045 self._ticker = figiJSON["ticker"] 1046 1047 if requestPrice: 1048 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1049 1050 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1051 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1052 1053 else: 1054 figiJSON["currentPrice"]["changes"] = 0 1055 1056 if show: 1057 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1058 1059 else: 1060 if show: 1061 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1062 1063 return figiJSON 1064 1065 def GetCurrentPrices(self, show: bool = True) -> dict: 1066 """ 1067 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1068 `{"buy": [{"price": 1243.8, "quantity": 193}, 1069 {"price": 1244.0, "quantity": 168}, 1070 {"price": 1244.8, "quantity": 5}, 1071 {"price": 1245.0, "quantity": 61}, 1072 {"price": 1245.4, "quantity": 60}], 1073 "sell": [{"price": 1243.6, "quantity": 8}, 1074 {"price": 1242.6, "quantity": 10}, 1075 {"price": 1242.4, "quantity": 18}, 1076 {"price": 1242.2, "quantity": 50}, 1077 {"price": 1242.0, "quantity": 113}], 1078 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1079 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1080 - sell: list of dicts with Buyers prices, 1081 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1082 - quantity: volume value by current price in lots, 1083 - limitUp: current trade session limit price, maximum, 1084 - limitDown: current trade session limit price, minimum, 1085 - lastPrice: last deal price of the instrument, 1086 - closePrice: previous trade session close price of the instrument. 1087 1088 See also: `SearchByTicker()` and `SearchByFIGI()`. 1089 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1090 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1091 1092 :param show: if `True` then print DOM to log and console. 1093 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1094 If an error occurred then returns an empty record: 1095 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1096 """ 1097 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1098 1099 if self.depth < 1: 1100 uLogger.error("Depth of Market (DOM) must be >=1!") 1101 raise Exception("Incorrect value") 1102 1103 if not (self._ticker or self._figi): 1104 uLogger.error("self._ticker or self._figi variables must be defined!") 1105 raise Exception("Ticker or FIGI required") 1106 1107 if self._ticker and not self._figi: 1108 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1109 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1110 1111 if not self._ticker and self._figi: 1112 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1113 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1114 1115 if not self._figi: 1116 uLogger.error("FIGI is not defined!") 1117 raise Exception("Ticker or FIGI required") 1118 1119 else: 1120 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1121 1122 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1123 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1124 self.body = str({"figi": self._figi, "depth": self.depth}) 1125 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1126 1127 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1128 # list of dicts with sellers orders: 1129 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1130 1131 # list of dicts with buyers orders: 1132 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1133 1134 # max price of instrument at this time: 1135 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1136 1137 # min price of instrument at this time: 1138 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1139 1140 # last price of deal with instrument: 1141 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1142 1143 # last close price of instrument: 1144 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1145 1146 else: 1147 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1148 uLogger.debug("Server response: {}".format(pricesResponse)) 1149 1150 if show: 1151 if prices["buy"] or prices["sell"]: 1152 info = [ 1153 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1154 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1155 self._ticker, 1156 self._figi, 1157 self.depth, 1158 ), 1159 "-" * 60, "\n", 1160 " Orders of Buyers | Orders of Sellers\n", 1161 "-" * 60, "\n", 1162 " Sell prices (volumes) | Buy prices (volumes)\n", 1163 "-" * 60, "\n", 1164 ] 1165 1166 if not prices["buy"]: 1167 info.append(" | No orders!\n") 1168 sumBuy = 0 1169 1170 else: 1171 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1172 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1173 for item in maxMinSorted: 1174 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1175 1176 if not prices["sell"]: 1177 info.append("No orders! |\n") 1178 sumSell = 0 1179 1180 else: 1181 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1182 for item in prices["sell"]: 1183 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1184 1185 info.extend([ 1186 "-" * 60, "\n", 1187 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1188 "-" * 60, "\n", 1189 ]) 1190 1191 infoText = "".join(info) 1192 1193 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1194 1195 else: 1196 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1197 1198 return prices 1199 1200 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1201 """ 1202 This method get and show information about all available broker instruments for current user account. 1203 If `instrumentsFile` string is not empty then also save information to this file. 1204 1205 :param show: if `True` then print results to console, if `False` — print only to file. 1206 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1207 :return: multi-lines string with all available broker instruments. 1208 """ 1209 if not self.iList: 1210 self.iList = self.Listing() 1211 1212 info = [ 1213 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1214 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1215 ] 1216 1217 # add instruments count by type: 1218 for iType in self.iList.keys(): 1219 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1220 1221 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1222 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1223 1224 # generating info tables with all instruments by type: 1225 for iType in self.iList.keys(): 1226 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1227 1228 for instrument in self.iList[iType].keys(): 1229 iName = self.iList[iType][instrument]["name"] # instrument's name 1230 if len(iName) > 57: 1231 iName = "{}...".format(iName[:54]) # right trim for a long string 1232 1233 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1234 self.iList[iType][instrument]["ticker"], 1235 iName, 1236 self.iList[iType][instrument]["figi"], 1237 self.iList[iType][instrument]["currency"], 1238 self.iList[iType][instrument]["lot"], 1239 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1240 )) 1241 1242 infoText = "".join(info) 1243 1244 if show and not onlyFiles: 1245 uLogger.info(infoText) 1246 1247 if self.instrumentsFile and (show or onlyFiles): 1248 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1249 fH.write(infoText) 1250 1251 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1252 1253 if self.useHTMLReports: 1254 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1255 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1256 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1257 1258 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1259 1260 return infoText 1261 1262 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1263 """ 1264 This method search and show information about instruments by part of its ticker, FIGI or name. 1265 If `searchResultsFile` string is not empty then also save information to this file. 1266 1267 :param pattern: string with part of ticker, FIGI or instrument's name. 1268 :param show: if `True` then print results to console, if `False` — return list of result only. 1269 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1270 :return: list of dictionaries with all found instruments. 1271 """ 1272 if not self.iList: 1273 self.iList = self.Listing() 1274 1275 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1276 compiledPattern = re.compile(pattern, re.IGNORECASE) 1277 1278 for iType in self.iList: 1279 for instrument in self.iList[iType].values(): 1280 searchResult = compiledPattern.search(" ".join( 1281 [instrument["ticker"], instrument["figi"], instrument["name"]] 1282 )) 1283 1284 if searchResult: 1285 searchResults[iType][instrument["ticker"]] = instrument 1286 1287 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1288 info = [ 1289 "# Search results\n\n", 1290 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1291 "* **Search pattern:** [{}]\n".format(pattern), 1292 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1293 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1294 ] 1295 infoShort = info[:] 1296 1297 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1298 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1299 skippedLine = "| ... | ... | ... | ... |\n" 1300 1301 if resultsLen == 0: 1302 info.append("\nNo results\n") 1303 infoShort.append("\nNo results\n") 1304 uLogger.warning("No results. Try changing your search pattern.") 1305 1306 else: 1307 for iType in searchResults: 1308 iTypeValuesCount = len(searchResults[iType].values()) 1309 if iTypeValuesCount > 0: 1310 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1311 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1312 1313 for instrument in searchResults[iType].values(): 1314 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1315 instrument["type"], 1316 instrument["ticker"], 1317 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1318 instrument["figi"], 1319 )) 1320 1321 if iTypeValuesCount <= 5: 1322 infoShort.extend(info[-iTypeValuesCount:]) 1323 1324 else: 1325 infoShort.extend(info[-5:]) 1326 infoShort.append(skippedLine) 1327 1328 infoText = "".join(info) 1329 infoTextShort = "".join(infoShort) 1330 1331 if show and not onlyFiles: 1332 uLogger.info(infoTextShort) 1333 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1334 1335 if self.searchResultsFile and (show or onlyFiles): 1336 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1337 fH.write(infoText) 1338 1339 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1340 1341 if self.useHTMLReports: 1342 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1343 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1344 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1345 1346 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1347 1348 return searchResults 1349 1350 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1351 """ 1352 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1353 1354 :param instruments: list of strings with tickers or FIGIs. 1355 :return: list with unique instrument FIGIs only. 1356 """ 1357 requestedInstruments = [] 1358 for iName in instruments: 1359 if iName not in self.aliases.keys(): 1360 if iName not in requestedInstruments: 1361 requestedInstruments.append(iName) 1362 1363 else: 1364 if iName not in requestedInstruments: 1365 if self.aliases[iName] not in requestedInstruments: 1366 requestedInstruments.append(self.aliases[iName]) 1367 1368 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1369 1370 onlyUniqueFIGIs = [] 1371 for iName in requestedInstruments: 1372 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1373 continue 1374 1375 self._ticker = iName 1376 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1377 1378 if not iData: 1379 self._ticker = "" 1380 self._figi = iName 1381 1382 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1383 1384 if not iData: 1385 self._figi = "" 1386 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1387 1388 if iData and iData["figi"] not in onlyUniqueFIGIs: 1389 onlyUniqueFIGIs.append(iData["figi"]) 1390 1391 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1392 1393 return onlyUniqueFIGIs 1394 1395 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1396 """ 1397 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1398 1399 See limits: https://tinkoff.github.io/investAPI/limits/ 1400 1401 If `pricesFile` string is not empty then also save information to this file. 1402 1403 :param instruments: list of strings with tickers or FIGIs. 1404 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1405 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1406 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1407 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1408 """ 1409 if instruments is None or not instruments: 1410 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1411 raise Exception("Ticker or FIGI required") 1412 1413 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1414 1415 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1416 1417 iList = [] # trying to get info and current prices about all unique instruments: 1418 for self._figi in onlyUniqueFIGIs: 1419 iData = self.SearchByFIGI(requestPrice=True, show=False) 1420 iList.append(iData) 1421 1422 self.ShowListOfPrices(iList, show, onlyFiles) 1423 1424 return iList 1425 1426 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1427 """ 1428 Show table contains current prices of given instruments. 1429 1430 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1431 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1432 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1433 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1434 :return: multilines text in Markdown format as a table contains current prices. 1435 """ 1436 infoText = "" 1437 1438 if show or self.pricesFile or onlyFiles: 1439 info = [ 1440 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1441 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1442 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1443 ] 1444 1445 for item in iList: 1446 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1447 item["ticker"], 1448 item["figi"], 1449 item["type"], 1450 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1451 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1452 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1453 "{} / {}".format( 1454 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1455 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1456 ), 1457 "{} / {}".format( 1458 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1459 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1460 ), 1461 item["currency"], 1462 )) 1463 1464 infoText = "".join(info) 1465 1466 if show and not onlyFiles: 1467 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1468 1469 if self.pricesFile and (show or onlyFiles): 1470 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1471 fH.write(infoText) 1472 1473 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1474 1475 if self.useHTMLReports: 1476 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1477 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1478 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1479 1480 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1481 1482 return infoText 1483 1484 def RequestTradingStatus(self) -> dict: 1485 """ 1486 Requesting trading status for the instrument defined by `figi` variable. 1487 1488 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1489 1490 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1491 1492 :return: dictionary with trading status attributes. Response example: 1493 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1494 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1495 """ 1496 if self._figi is None or not self._figi: 1497 uLogger.error("Variable `figi` must be defined for using this method!") 1498 raise Exception("FIGI required") 1499 1500 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1501 1502 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1503 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1504 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1505 1506 if self.moreDebug: 1507 uLogger.debug("Records about current trading status successfully received") 1508 1509 return tradingStatus 1510 1511 def RequestPortfolio(self) -> dict: 1512 """ 1513 Requesting actual user's portfolio for current `accountId`. 1514 1515 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1516 1517 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1518 1519 :return: dictionary with user's portfolio. 1520 """ 1521 if self.accountId is None or not self.accountId: 1522 uLogger.error("Variable `accountId` must be defined for using this method!") 1523 raise Exception("Account ID required") 1524 1525 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1526 1527 self.body = str({"accountId": self.accountId}) 1528 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1529 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1530 1531 if self.moreDebug: 1532 uLogger.debug("Records about user's portfolio successfully received") 1533 1534 return rawPortfolio 1535 1536 def RequestPositions(self) -> dict: 1537 """ 1538 Requesting open positions by currencies and instruments for current `accountId`. 1539 1540 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1541 1542 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1543 1544 :return: dictionary with open positions by instruments. 1545 """ 1546 if self.accountId is None or not self.accountId: 1547 uLogger.error("Variable `accountId` must be defined for using this method!") 1548 raise Exception("Account ID required") 1549 1550 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1551 1552 self.body = str({"accountId": self.accountId}) 1553 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1554 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1555 1556 if self.moreDebug: 1557 uLogger.debug("Records about current open positions successfully received") 1558 1559 return rawPositions 1560 1561 def RequestPendingOrders(self) -> list: 1562 """ 1563 Requesting current actual pending limit orders for current `accountId`. 1564 1565 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1566 1567 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1568 1569 :return: list of dictionaries with pending limit orders. 1570 """ 1571 if self.accountId is None or not self.accountId: 1572 uLogger.error("Variable `accountId` must be defined for using this method!") 1573 raise Exception("Account ID required") 1574 1575 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1576 1577 self.body = str({"accountId": self.accountId}) 1578 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1579 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1580 1581 if "orders" in rawResponse.keys(): 1582 rawOrders = rawResponse["orders"] 1583 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1584 1585 else: 1586 rawOrders = [] 1587 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1588 1589 return rawOrders 1590 1591 def RequestStopOrders(self) -> list: 1592 """ 1593 Requesting current actual stop orders for current `accountId`. 1594 1595 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1596 1597 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1598 1599 :return: list of dictionaries with stop orders. 1600 """ 1601 if self.accountId is None or not self.accountId: 1602 uLogger.error("Variable `accountId` must be defined for using this method!") 1603 raise Exception("Account ID required") 1604 1605 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1606 1607 self.body = str({"accountId": self.accountId}) 1608 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1609 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1610 1611 if "stopOrders" in rawResponse.keys(): 1612 rawStopOrders = rawResponse["stopOrders"] 1613 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1614 1615 else: 1616 rawStopOrders = [] 1617 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1618 1619 return rawStopOrders 1620 1621 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1622 """ 1623 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1624 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1625 and `overviewBondsCalendarFile` are defined then also save information to file. 1626 1627 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1628 many requests about the state of the portfolio, and then, based on the received data, a large number 1629 of calculation and statistics are collected. 1630 1631 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1632 :param details: how detailed should the information be? 1633 - `full` — shows full available information about portfolio status (by default), 1634 - `positions` — shows only open positions, 1635 - `orders` — shows only sections of open limits and stop orders. 1636 - `digest` — show a short digest of the portfolio status, 1637 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1638 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1639 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1640 :return: dictionary with client's raw portfolio and some statistics. 1641 """ 1642 if self.accountId is None or not self.accountId: 1643 uLogger.error("Variable `accountId` must be defined for using this method!") 1644 raise Exception("Account ID required") 1645 1646 view = { 1647 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1648 "headers": {}, # list of dictionaries, response headers without "positions" section 1649 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1650 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1651 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1652 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1653 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1654 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1655 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1656 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1657 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1658 }, 1659 "stat": { # --- some statistics calculated using "raw" sections: 1660 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1661 "availableRUB": 0., # available rubles (without other currencies) 1662 "blockedRUB": 0., # blocked sum in Russian Rouble 1663 "totalChangesRUB": 0., # changes for all open trades in RUB 1664 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1665 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1666 "sharesCostRUB": 0., # costs of all shares in RUB 1667 "bondsCostRUB": 0., # costs of all bonds in RUB 1668 "etfsCostRUB": 0., # costs of all etfs in RUB 1669 "futuresCostRUB": 0., # costs of all futures in RUB 1670 "Currencies": [], # list of dictionaries of all currencies statistics 1671 "Shares": [], # list of dictionaries of all shares statistics 1672 "Bonds": [], # list of dictionaries of all bonds statistics 1673 "Etfs": [], # list of dictionaries of all etfs statistics 1674 "Futures": [], # list of dictionaries of all futures statistics 1675 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1676 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1677 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1678 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1679 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1680 }, 1681 "analytics": { # --- some analytics of portfolio: 1682 "distrByAssets": {}, # portfolio distribution by assets 1683 "distrByCompanies": {}, # portfolio distribution by companies 1684 "distrBySectors": {}, # portfolio distribution by sectors 1685 "distrByCurrencies": {}, # portfolio distribution by currencies 1686 "distrByCountries": {}, # portfolio distribution by countries 1687 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1688 } 1689 } 1690 1691 details = details.lower() 1692 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1693 if details not in availableDetails: 1694 details = "full" 1695 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1696 1697 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1698 1699 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1700 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1701 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1702 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1703 1704 # save response headers without "positions" section: 1705 for key in portfolioResponse.keys(): 1706 if key != "positions": 1707 view["raw"]["headers"][key] = portfolioResponse[key] 1708 1709 else: 1710 continue 1711 1712 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1713 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1714 for item in portfolioResponse["positions"]: 1715 if item["instrumentType"] == "currency": 1716 self._figi = item["figi"] 1717 if not self._figi and item["ticker"]: 1718 self._ticker = item["ticker"] 1719 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1720 1721 curr = self.SearchByFIGI(requestPrice=False) 1722 1723 # current price of currency in RUB: 1724 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1725 "name": curr["name"], 1726 "currentPrice": NanoToFloat( 1727 item["currentPrice"]["units"], 1728 item["currentPrice"]["nano"] 1729 ), 1730 } 1731 1732 view["raw"]["Currencies"].append(item) 1733 1734 elif item["instrumentType"] == "share": 1735 view["raw"]["Shares"].append(item) 1736 1737 elif item["instrumentType"] == "bond": 1738 view["raw"]["Bonds"].append(item) 1739 1740 elif item["instrumentType"] == "etf": 1741 view["raw"]["Etfs"].append(item) 1742 1743 elif item["instrumentType"] == "futures": 1744 view["raw"]["Futures"].append(item) 1745 1746 else: 1747 continue 1748 1749 # how many volume of currencies (by ISO currency name) are blocked: 1750 for item in view["raw"]["positions"]["blocked"]: 1751 blocked = NanoToFloat(item["units"], item["nano"]) 1752 if blocked > 0: 1753 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1754 1755 # how many volume of instruments (by FIGI) are blocked: 1756 for item in view["raw"]["positions"]["securities"]: 1757 blocked = int(item["blocked"]) 1758 if blocked > 0: 1759 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1760 1761 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1762 1763 if "rub" in allBlocked.keys(): 1764 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1765 1766 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1767 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1768 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1769 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1770 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1771 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1772 view["stat"]["portfolioCostRUB"] = sum([ 1773 view["stat"]["allCurrenciesCostRUB"], 1774 view["stat"]["sharesCostRUB"], 1775 view["stat"]["bondsCostRUB"], 1776 view["stat"]["etfsCostRUB"], 1777 view["stat"]["futuresCostRUB"], 1778 ]) 1779 1780 # --- calculating some portfolio statistics: 1781 byComp = {} # distribution by companies 1782 bySect = {} # distribution by sectors 1783 byCurr = {} # distribution by currencies (include RUB) 1784 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1785 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1786 1787 for item in portfolioResponse["positions"]: 1788 self._figi = item["figi"] 1789 if not self._figi and item["ticker"]: 1790 self._ticker = item["ticker"] 1791 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1792 1793 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1794 1795 if instrument: 1796 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1797 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1798 1799 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1800 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1801 1802 else: 1803 blocked = 0 1804 1805 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1806 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1807 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1808 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1809 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1810 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1811 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1812 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1813 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1814 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1815 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1816 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1817 1818 statData = { 1819 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1820 "ticker": instrument["ticker"], # ticker by FIGI 1821 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1822 "volume": volume, # available volume of instrument 1823 "lots": lots, # volume in lots of instrument 1824 "direction": direction, # direction of an instrument's position: short or long 1825 "blocked": blocked, # blocked volume of currency or instrument 1826 "currentPrice": curPrice, # current instrument's price in basic asset 1827 "average": average, # current average position price 1828 "cost": cost, # current cost of all volume of instrument in basic asset 1829 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1830 "costRUB": costRUB, # cost of instrument in ruble 1831 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1832 "profit": profit, # expected profit at current moment 1833 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1834 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1835 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1836 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1837 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1838 "step": instrument["step"], # minimum price increment 1839 } 1840 1841 # adding distribution by unique countries: 1842 if statData["country"] not in byCountry.keys(): 1843 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1844 1845 else: 1846 byCountry[statData["country"]]["cost"] += costRUB 1847 byCountry[statData["country"]]["percent"] += percentCostRUB 1848 1849 if item["instrumentType"] != "currency": 1850 # adding distribution by unique companies: 1851 if statData["name"]: 1852 if statData["name"] not in byComp.keys(): 1853 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1854 1855 else: 1856 byComp[statData["name"]]["cost"] += costRUB 1857 byComp[statData["name"]]["percent"] += percentCostRUB 1858 1859 # adding distribution by unique sectors: 1860 if statData["sector"] not in bySect.keys(): 1861 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1862 1863 else: 1864 bySect[statData["sector"]]["cost"] += costRUB 1865 bySect[statData["sector"]]["percent"] += percentCostRUB 1866 1867 # adding distribution by unique currencies: 1868 if currency not in byCurr.keys(): 1869 byCurr[currency] = { 1870 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1871 "cost": costRUB, 1872 "percent": percentCostRUB 1873 } 1874 1875 else: 1876 byCurr[currency]["cost"] += costRUB 1877 byCurr[currency]["percent"] += percentCostRUB 1878 1879 # saving statistics for every instrument: 1880 if item["instrumentType"] == "currency": 1881 view["stat"]["Currencies"].append(statData) 1882 1883 # update dict with free funds for trading (total - blocked) by currencies 1884 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1885 view["stat"]["funds"][currency] = { 1886 "total": volume, 1887 "totalCostRUB": costRUB, # total volume cost in rubles 1888 "free": volume - blocked, 1889 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1890 } 1891 1892 elif item["instrumentType"] == "share": 1893 view["stat"]["Shares"].append(statData) 1894 1895 elif item["instrumentType"] == "bond": 1896 view["stat"]["Bonds"].append(statData) 1897 1898 elif item["instrumentType"] == "etf": 1899 view["stat"]["Etfs"].append(statData) 1900 1901 elif item["instrumentType"] == "Futures": 1902 view["stat"]["Futures"].append(statData) 1903 1904 else: 1905 continue 1906 1907 # total changes in Russian Ruble: 1908 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1909 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1910 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1911 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1912 view["stat"]["funds"]["rub"] = { 1913 "total": view["stat"]["availableRUB"], 1914 "totalCostRUB": view["stat"]["availableRUB"], 1915 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1916 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1917 } 1918 1919 # --- pending limit orders sector data: 1920 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1921 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1922 1923 for item in view["raw"]["orders"]: 1924 self._figi = item["figi"] 1925 1926 if item["figi"] not in uniquePendingOrdersFIGIs: 1927 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1928 1929 uniquePendingOrdersFIGIs.append(item["figi"]) 1930 uniquePendingOrders[item["figi"]] = instrument 1931 1932 else: 1933 instrument = uniquePendingOrders[item["figi"]] 1934 1935 if instrument: 1936 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1937 orderType = TKS_ORDER_TYPES[item["orderType"]] 1938 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1939 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1940 1941 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1942 if item["direction"] == "ORDER_DIRECTION_BUY": 1943 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1944 1945 else: 1946 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1947 1948 # requested price for order execution: 1949 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1950 1951 # necessary changes in percent to reach target from current price: 1952 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1953 1954 view["stat"]["orders"].append({ 1955 "orderID": item["orderId"], # orderId number parameter of current order 1956 "figi": item["figi"], # FIGI identification 1957 "ticker": instrument["ticker"], # ticker name by FIGI 1958 "lotsRequested": item["lotsRequested"], # requested lots value 1959 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1960 "currentPrice": lastPrice, # current instrument's price for defined action 1961 "targetPrice": target, # requested price for order execution in base currency 1962 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1963 "percentChanges": changes, # changes in percent to target from current price 1964 "currency": item["currency"], # instrument's currency name 1965 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1966 "type": orderType, # type of order from TKS_ORDER_TYPES 1967 "status": orderState, # order status from TKS_ORDER_STATES 1968 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1969 }) 1970 1971 # --- stop orders sector data: 1972 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1973 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1974 1975 for item in view["raw"]["stopOrders"]: 1976 self._figi = item["figi"] 1977 1978 if item["figi"] not in uniqueStopOrdersFIGIs: 1979 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1980 1981 uniqueStopOrdersFIGIs.append(item["figi"]) 1982 uniqueStopOrders[item["figi"]] = instrument 1983 1984 else: 1985 instrument = uniqueStopOrders[item["figi"]] 1986 1987 if instrument: 1988 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1989 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1990 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1991 1992 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1993 if "expirationTime" in item.keys(): 1994 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1995 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1996 1997 else: 1998 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1999 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 2000 2001 # current instrument's price (last sellers order if buy, and last buyers order if sell): 2002 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 2003 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2004 2005 else: 2006 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2007 2008 # requested price when stop-order executed: 2009 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2010 2011 # price for limit-order, set up when stop-order executed: 2012 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2013 2014 # necessary changes in percent to reach target from current price: 2015 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2016 2017 view["stat"]["stopOrders"].append({ 2018 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2019 "figi": item["figi"], # FIGI identification 2020 "ticker": instrument["ticker"], # ticker name by FIGI 2021 "lotsRequested": item["lotsRequested"], # requested lots value 2022 "currentPrice": lastPrice, # current instrument's price for defined action 2023 "targetPrice": target, # requested price for stop-order execution in base currency 2024 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2025 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2026 "percentChanges": changes, # changes in percent to target from current price 2027 "currency": item["currency"], # instrument's currency name 2028 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2029 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2030 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2031 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2032 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2033 }) 2034 2035 # --- calculating data for analytics section: 2036 # portfolio distribution by assets: 2037 view["analytics"]["distrByAssets"] = { 2038 "Ruble": { 2039 "uniques": 1, 2040 "cost": view["stat"]["availableRUB"], 2041 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2042 }, 2043 "Currencies": { 2044 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2045 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2046 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2047 }, 2048 "Shares": { 2049 "uniques": len(view["stat"]["Shares"]), 2050 "cost": view["stat"]["sharesCostRUB"], 2051 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2052 }, 2053 "Bonds": { 2054 "uniques": len(view["stat"]["Bonds"]), 2055 "cost": view["stat"]["bondsCostRUB"], 2056 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2057 }, 2058 "Etfs": { 2059 "uniques": len(view["stat"]["Etfs"]), 2060 "cost": view["stat"]["etfsCostRUB"], 2061 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2062 }, 2063 "Futures": { 2064 "uniques": len(view["stat"]["Futures"]), 2065 "cost": view["stat"]["futuresCostRUB"], 2066 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2067 }, 2068 } 2069 2070 # portfolio distribution by companies: 2071 view["analytics"]["distrByCompanies"]["All money cash"] = { 2072 "ticker": "", 2073 "cost": view["stat"]["allCurrenciesCostRUB"], 2074 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2075 } 2076 view["analytics"]["distrByCompanies"].update(byComp) 2077 2078 # portfolio distribution by sectors: 2079 view["analytics"]["distrBySectors"]["All money cash"] = { 2080 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2081 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2082 } 2083 view["analytics"]["distrBySectors"].update(bySect) 2084 2085 # portfolio distribution by currencies: 2086 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2087 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2088 2089 if self.moreDebug: 2090 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2091 2092 view["analytics"]["distrByCurrencies"].update(byCurr) 2093 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2094 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2095 2096 # portfolio distribution by countries: 2097 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2098 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2099 2100 if self.moreDebug: 2101 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2102 2103 view["analytics"]["distrByCountries"].update(byCountry) 2104 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2105 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2106 2107 # --- Prepare text statistics overview in human-readable: 2108 if show or onlyFiles: 2109 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2110 2111 # Whatever the value `details`, header not changes: 2112 info = [ 2113 "# Client's portfolio\n\n", 2114 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2115 "* **Account ID:** [{}]\n".format(self.accountId), 2116 ] 2117 2118 if details in ["full", "positions", "digest"]: 2119 info.extend([ 2120 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2121 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2122 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2123 view["stat"]["totalChangesRUB"], 2124 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2125 view["stat"]["totalChangesPercentRUB"], 2126 ), 2127 ]) 2128 2129 if details in ["full", "positions"]: 2130 info.extend([ 2131 "## Open positions\n\n", 2132 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2133 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2134 "| **Ruble:** | {:>31} | | | | | |\n".format( 2135 "{:.2f} ({:.2f}) rub".format( 2136 view["stat"]["availableRUB"], 2137 view["stat"]["blockedRUB"], 2138 ) 2139 ) 2140 ]) 2141 2142 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2143 return [ 2144 "| | | | | | | |\n", 2145 "| {:<27} | | | | | {:>19} | |\n".format( 2146 noTradeStr if noTradeStr else typeStr, 2147 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2148 ), 2149 ] 2150 2151 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2152 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2153 "{} [{}]".format(data["ticker"], data["figi"]), 2154 "{:.2f} ({:.2f}) {}".format( 2155 data["volume"], 2156 data["blocked"], 2157 data["currency"], 2158 ) if isCurr else "{:.0f} ({:.0f})".format( 2159 data["volume"], 2160 data["blocked"], 2161 ), 2162 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2163 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2164 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2165 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2166 "{}{:.2f} {} ({}{:.2f}%)".format( 2167 "+" if data["profit"] > 0 else "", 2168 data["profit"], data["baseCurrencyName"], 2169 "+" if data["percentProfit"] > 0 else "", 2170 data["percentProfit"], 2171 ), 2172 ) 2173 2174 # --- Show currencies section: 2175 if view["stat"]["Currencies"]: 2176 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2177 for item in view["stat"]["Currencies"]: 2178 info.append(_InfoStr(item, isCurr=True)) 2179 2180 else: 2181 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2182 2183 # --- Show shares section: 2184 if view["stat"]["Shares"]: 2185 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2186 2187 for item in view["stat"]["Shares"]: 2188 info.append(_InfoStr(item)) 2189 2190 else: 2191 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2192 2193 # --- Show bonds section: 2194 if view["stat"]["Bonds"]: 2195 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2196 2197 for item in view["stat"]["Bonds"]: 2198 info.append(_InfoStr(item)) 2199 2200 else: 2201 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2202 2203 # --- Show etfs section: 2204 if view["stat"]["Etfs"]: 2205 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2206 2207 for item in view["stat"]["Etfs"]: 2208 info.append(_InfoStr(item)) 2209 2210 else: 2211 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2212 2213 # --- Show futures section: 2214 if view["stat"]["Futures"]: 2215 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2216 2217 for item in view["stat"]["Futures"]: 2218 info.append(_InfoStr(item)) 2219 2220 else: 2221 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2222 2223 if details in ["full", "orders"]: 2224 # --- Show pending limit orders section: 2225 if view["stat"]["orders"]: 2226 info.extend([ 2227 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2228 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2229 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2230 ]) 2231 2232 for item in view["stat"]["orders"]: 2233 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2234 "{} [{}]".format(item["ticker"], item["figi"]), 2235 item["orderID"], 2236 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2237 "{} {} ({}{:.2f}%)".format( 2238 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2239 item["baseCurrencyName"], 2240 "+" if item["percentChanges"] > 0 else "", 2241 float(item["percentChanges"]), 2242 ), 2243 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2244 item["action"], 2245 item["type"], 2246 item["date"], 2247 )) 2248 2249 else: 2250 info.append("\n## Total pending limit-orders: [0]\n") 2251 2252 # --- Show stop orders section: 2253 if view["stat"]["stopOrders"]: 2254 info.extend([ 2255 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2256 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2257 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2258 ]) 2259 2260 for item in view["stat"]["stopOrders"]: 2261 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2262 "{} [{}]".format(item["ticker"], item["figi"]), 2263 item["orderID"], 2264 item["lotsRequested"], 2265 "{} {} ({}{:.2f}%)".format( 2266 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2267 item["baseCurrencyName"], 2268 "+" if item["percentChanges"] > 0 else "", 2269 float(item["percentChanges"]), 2270 ), 2271 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2272 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2273 item["action"], 2274 item["type"], 2275 item["expType"], 2276 item["createDate"], 2277 item["expDate"], 2278 )) 2279 2280 else: 2281 info.append("\n## Total stop-orders: [0]\n") 2282 2283 if details in ["full", "analytics"]: 2284 # -- Show analytics section: 2285 if view["stat"]["portfolioCostRUB"] > 0: 2286 info.extend([ 2287 "\n# Analytics\n\n" 2288 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2289 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2290 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2291 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2292 view["stat"]["totalChangesRUB"], 2293 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2294 view["stat"]["totalChangesPercentRUB"], 2295 ), 2296 "\n## Portfolio distribution by assets\n" 2297 "\n| Type | Uniques | Percent | Current cost |\n", 2298 "|------------------------------------|---------|---------|--------------------|\n", 2299 ]) 2300 2301 for key in view["analytics"]["distrByAssets"].keys(): 2302 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2303 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2304 key, 2305 view["analytics"]["distrByAssets"][key]["uniques"], 2306 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2307 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2308 )) 2309 2310 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2311 2312 info.extend([ 2313 "\n## Portfolio distribution by companies\n" 2314 "\n| Company | Percent | Current cost |\n", 2315 aSepLine, 2316 ]) 2317 2318 for company in view["analytics"]["distrByCompanies"].keys(): 2319 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2320 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2321 "{}{}".format( 2322 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2323 company, 2324 ), 2325 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2326 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2327 )) 2328 2329 info.extend([ 2330 "\n## Portfolio distribution by sectors\n" 2331 "\n| Sector | Percent | Current cost |\n", 2332 aSepLine, 2333 ]) 2334 2335 for sector in view["analytics"]["distrBySectors"].keys(): 2336 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2337 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2338 sector, 2339 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2340 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2341 )) 2342 2343 info.extend([ 2344 "\n## Portfolio distribution by currencies\n" 2345 "\n| Instruments currencies | Percent | Current cost |\n", 2346 aSepLine, 2347 ]) 2348 2349 for curr in view["analytics"]["distrByCurrencies"].keys(): 2350 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2351 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2352 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2353 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2354 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2355 )) 2356 2357 info.extend([ 2358 "\n## Portfolio distribution by countries\n" 2359 "\n| Assets by country | Percent | Current cost |\n", 2360 aSepLine, 2361 ]) 2362 2363 for country in view["analytics"]["distrByCountries"].keys(): 2364 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2365 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2366 country, 2367 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2368 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2369 )) 2370 2371 if details in ["full", "calendar"]: 2372 # -- Show bonds payment calendar section: 2373 if view["stat"]["Bonds"]: 2374 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2375 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2376 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2377 2378 else: 2379 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2380 2381 infoText = "".join(info) 2382 2383 if show and not onlyFiles: 2384 uLogger.info(infoText) 2385 2386 if details == "full" and self.overviewFile: 2387 filename = self.overviewFile 2388 2389 elif details == "digest" and self.overviewDigestFile: 2390 filename = self.overviewDigestFile 2391 2392 elif details == "positions" and self.overviewPositionsFile: 2393 filename = self.overviewPositionsFile 2394 2395 elif details == "orders" and self.overviewOrdersFile: 2396 filename = self.overviewOrdersFile 2397 2398 elif details == "analytics" and self.overviewAnalyticsFile: 2399 filename = self.overviewAnalyticsFile 2400 2401 elif details == "calendar" and self.overviewBondsCalendarFile: 2402 filename = self.overviewBondsCalendarFile 2403 2404 else: 2405 filename = "" 2406 2407 if filename and (show or onlyFiles): 2408 with open(filename, "w", encoding="UTF-8") as fH: 2409 fH.write(infoText) 2410 2411 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2412 2413 if self.useHTMLReports: 2414 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2415 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2416 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2417 2418 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2419 2420 return view 2421 2422 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2423 """ 2424 Returns history operations between two given dates for current `accountId`. 2425 If `reportFile` string is not empty then also save human-readable report. 2426 Shows some statistical data of closed positions. 2427 2428 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2429 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2430 :param show: if `True` then also prints all records to the console. 2431 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2432 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2433 :return: original list of dictionaries with history of deals records from API ("operations" key): 2434 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2435 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2436 """ 2437 if self.accountId is None or not self.accountId: 2438 uLogger.error("Variable `accountId` must be defined for using this method!") 2439 raise Exception("Account ID required") 2440 2441 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2442 2443 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2444 2445 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2446 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2447 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2448 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2449 customStat = {} # custom statistics in additional to responseJSON 2450 2451 # --- output report in human-readable format: 2452 if self.reportFile and (show or onlyFiles): 2453 splitLine1 = "| | | | | |\n" # Summary section 2454 splitLine2 = "| | | | | | | | |\n" # Operations section 2455 nextDay = "" 2456 2457 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2458 2459 if len(ops) > 0: 2460 customStat = { 2461 "opsCount": 0, # total operations count 2462 "buyCount": 0, # buy operations 2463 "sellCount": 0, # sell operations 2464 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2465 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2466 "payIn": {"rub": 0.}, # Deposit brokerage account 2467 "payOut": {"rub": 0.}, # Withdrawals 2468 "divs": {"rub": 0.}, # Dividends income 2469 "coupons": {"rub": 0.}, # Coupon's income 2470 "brokerCom": {"rub": 0.}, # Service commissions 2471 "serviceCom": {"rub": 0.}, # Service commissions 2472 "marginCom": {"rub": 0.}, # Margin commissions 2473 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2474 } 2475 2476 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2477 for item in ops: 2478 if item["state"] == "OPERATION_STATE_EXECUTED": 2479 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2480 2481 # count buy operations: 2482 if "_BUY" in item["operationType"]: 2483 customStat["buyCount"] += 1 2484 2485 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2486 customStat["buyTotal"][item["payment"]["currency"]] += payment 2487 2488 else: 2489 customStat["buyTotal"][item["payment"]["currency"]] = payment 2490 2491 # count sell operations: 2492 elif "_SELL" in item["operationType"]: 2493 customStat["sellCount"] += 1 2494 2495 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2496 customStat["sellTotal"][item["payment"]["currency"]] += payment 2497 2498 else: 2499 customStat["sellTotal"][item["payment"]["currency"]] = payment 2500 2501 # count incoming operations: 2502 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2503 if item["payment"]["currency"] in customStat["payIn"].keys(): 2504 customStat["payIn"][item["payment"]["currency"]] += payment 2505 2506 else: 2507 customStat["payIn"][item["payment"]["currency"]] = payment 2508 2509 # count withdrawals operations: 2510 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2511 if item["payment"]["currency"] in customStat["payOut"].keys(): 2512 customStat["payOut"][item["payment"]["currency"]] += payment 2513 2514 else: 2515 customStat["payOut"][item["payment"]["currency"]] = payment 2516 2517 # count dividends income: 2518 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2519 if item["payment"]["currency"] in customStat["divs"].keys(): 2520 customStat["divs"][item["payment"]["currency"]] += payment 2521 2522 else: 2523 customStat["divs"][item["payment"]["currency"]] = payment 2524 2525 # count coupon's income: 2526 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2527 if item["payment"]["currency"] in customStat["coupons"].keys(): 2528 customStat["coupons"][item["payment"]["currency"]] += payment 2529 2530 else: 2531 customStat["coupons"][item["payment"]["currency"]] = payment 2532 2533 # count broker commissions: 2534 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2535 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2536 customStat["brokerCom"][item["payment"]["currency"]] += payment 2537 2538 else: 2539 customStat["brokerCom"][item["payment"]["currency"]] = payment 2540 2541 # count service commissions: 2542 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2543 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2544 customStat["serviceCom"][item["payment"]["currency"]] += payment 2545 2546 else: 2547 customStat["serviceCom"][item["payment"]["currency"]] = payment 2548 2549 # count margin commissions: 2550 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2551 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2552 customStat["marginCom"][item["payment"]["currency"]] += payment 2553 2554 else: 2555 customStat["marginCom"][item["payment"]["currency"]] = payment 2556 2557 # count withholding taxes: 2558 elif "_TAX" in item["operationType"]: 2559 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2560 customStat["allTaxes"][item["payment"]["currency"]] += payment 2561 2562 else: 2563 customStat["allTaxes"][item["payment"]["currency"]] = payment 2564 2565 else: 2566 continue 2567 2568 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2569 2570 # --- view "Actions" lines: 2571 info.extend([ 2572 "| Report sections | | | | |\n", 2573 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2574 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2575 "| | Buy: {:<22} | {:<28} | | |\n".format( 2576 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2577 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2578 ), 2579 "| | Sell: {:<21} | {:<28} | | |\n".format( 2580 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2581 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2582 ), 2583 ]) 2584 2585 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2586 for key in opsKeys: 2587 if key == "rub": 2588 continue 2589 2590 info.extend([ 2591 "| | | {:<28} | | |\n".format( 2592 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2593 ), 2594 "| | | {:<28} | | |\n".format( 2595 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2596 ), 2597 ]) 2598 2599 info.append(splitLine1) 2600 2601 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2602 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2603 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2604 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2605 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2606 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2607 ) 2608 2609 # --- view "Payments" lines: 2610 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2611 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2612 2613 for key in paymentsKeys: 2614 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2615 2616 info.append(splitLine1) 2617 2618 # --- view "Commissions and taxes" lines: 2619 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2620 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2621 2622 for key in comKeys: 2623 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2624 2625 info.extend([ 2626 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2627 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2628 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2629 ]) 2630 2631 else: 2632 info.append("Broker returned no operations during this period\n") 2633 2634 # --- view "Operations" section: 2635 for item in ops: 2636 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2637 continue 2638 2639 else: 2640 self._figi = item["figi"] 2641 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2642 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2643 2644 # group of deals during one day: 2645 if nextDay and item["date"].split("T")[0] != nextDay: 2646 info.append(splitLine2) 2647 nextDay = "" 2648 2649 else: 2650 nextDay = item["date"].split("T")[0] # saving current day for splitting 2651 2652 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2653 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2654 self._figi if self._figi else "—", 2655 instrument["ticker"] if instrument else "—", 2656 instrument["type"] if instrument else "—", 2657 item["quantity"] if int(item["quantity"]) > 0 else "—", 2658 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2659 TKS_OPERATION_STATES[item["state"]], 2660 TKS_OPERATION_TYPES[item["operationType"]], 2661 )) 2662 2663 infoText = "".join(info) 2664 2665 if show and not onlyFiles: 2666 if self.moreDebug: 2667 uLogger.debug("Records about history of a client's operations successfully received") 2668 2669 uLogger.info(infoText) 2670 2671 if self.reportFile and (show or onlyFiles): 2672 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2673 fH.write(infoText) 2674 2675 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2676 2677 if self.useHTMLReports: 2678 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2679 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2680 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2681 2682 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2683 2684 return ops, customStat 2685 2686 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2687 """ 2688 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2689 2690 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2691 Warning! Broker server used ISO UTC time by default. 2692 2693 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2694 Also, `historyFile` used to update history with `onlyMissing` parameter. 2695 2696 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2697 2698 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2699 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2700 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2701 `"hour"`, `"day"`. Default: `"hour"`. 2702 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2703 False by default. Warning! History appends only from last candle to current time 2704 with always update last candle! 2705 :param csvSep: separator if csv-file is used, `,` by default. 2706 :param show: if `True` then also prints Pandas DataFrame to the console. 2707 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2708 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2709 `["date", "time", "open", "high", "low", "close", "volume"]`. 2710 """ 2711 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2712 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2713 history = None # empty pandas object for history 2714 2715 if interval not in TKS_CANDLE_INTERVALS.keys(): 2716 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2717 raise Exception("Incorrect value") 2718 2719 if not (self._ticker or self._figi): 2720 uLogger.error("Ticker or FIGI must be defined!") 2721 raise Exception("Ticker or FIGI required") 2722 2723 if self._ticker and not self._figi: 2724 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2725 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2726 2727 if self._figi and not self._ticker: 2728 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2729 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2730 2731 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2732 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2733 if interval.lower() != "day": 2734 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2735 2736 delta = dtEnd - dtStart # current UTC time minus last time in file 2737 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2738 2739 # calculate history length in candles: 2740 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2741 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2742 length += 1 # to avoid fraction time 2743 2744 # calculate data blocks count: 2745 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2746 2747 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2748 if self.moreDebug: 2749 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2750 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2751 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2752 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2753 2754 tempOld = None # pandas object for old history, if --only-missing key present 2755 lastTime = None # datetime object of last old candle in file 2756 2757 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2758 if self.moreDebug: 2759 uLogger.debug("--only-missing key present, add only last missing candles...") 2760 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2761 2762 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2763 2764 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2765 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2766 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2767 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2768 2769 # get last datetime object from last string in file or minus 1 delta if file is empty: 2770 if len(tempOld) > 0: 2771 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2772 2773 else: 2774 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2775 2776 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2777 2778 responseJSONs = [] # raw history blocks of data 2779 2780 blockEnd = dtEnd 2781 for item in range(blocks): 2782 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2783 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2784 2785 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2786 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2787 )) 2788 2789 if blockStart == blockEnd: 2790 uLogger.debug("Skipped this zero-length block...") 2791 2792 else: 2793 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2794 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2795 self.body = str({ 2796 "figi": self._figi, 2797 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2798 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2799 "interval": TKS_CANDLE_INTERVALS[interval][0] 2800 }) 2801 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2802 2803 if "code" in responseJSON.keys(): 2804 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2805 2806 else: 2807 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2808 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2809 2810 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2811 2812 blockEnd = blockStart 2813 2814 printCount = len(responseJSONs) # candles to show in console 2815 if responseJSONs: 2816 tempHistory = pd.DataFrame( 2817 data={ 2818 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2819 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2820 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2821 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2822 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2823 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2824 "volume": [int(item["volume"]) for item in responseJSONs], 2825 }, 2826 index=range(len(responseJSONs)), 2827 columns=["date", "time", "open", "high", "low", "close", "volume"], 2828 ) 2829 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2830 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2831 2832 # append only newest candles to old history if --only-missing key present: 2833 if onlyMissing and tempOld is not None and lastTime is not None: 2834 index = 0 # find start index in tempHistory data: 2835 2836 for i, item in tempHistory.iterrows(): 2837 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2838 2839 if curTime == lastTime: 2840 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2841 index = i 2842 printCount = index + 1 2843 break 2844 2845 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2846 2847 else: 2848 history = tempHistory # if no `--only-missing` key then load full data from server 2849 2850 if self.moreDebug: 2851 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2852 2853 if history is not None and not history.empty: 2854 if show and not onlyFiles: 2855 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2856 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2857 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2858 )) 2859 2860 else: 2861 uLogger.warning("Received an empty candles history!") 2862 2863 if self.historyFile is not None: 2864 if history is not None and not history.empty: 2865 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2866 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2867 2868 else: 2869 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2870 2871 else: 2872 if self.moreDebug: 2873 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2874 2875 return history 2876 2877 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2878 """ 2879 Load candles history from csv-file and return Pandas DataFrame object. 2880 2881 See also: `History()` and `ShowHistoryChart()` methods. 2882 2883 :param filePath: path to csv-file to open. 2884 """ 2885 loadedHistory = None # init candles data object 2886 2887 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2888 2889 if os.path.exists(filePath): 2890 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2891 2892 tfStr = self.priceModel.FormattedDelta( 2893 self.priceModel.timeframe, 2894 "{days} days {hours}h {minutes}m {seconds}s", 2895 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2896 self.priceModel.timeframe, 2897 "{hours}h {minutes}m {seconds}s", 2898 ) 2899 2900 if loadedHistory is not None and not loadedHistory.empty: 2901 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2902 len(loadedHistory), 2903 tfStr, 2904 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2905 ) 2906 2907 else: 2908 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2909 2910 else: 2911 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2912 2913 return loadedHistory 2914 2915 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2916 """ 2917 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2918 2919 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2920 Default: `index.html` (both for interact and non-interact candlesticks chart). 2921 2922 See also: `History()` and `LoadHistory()` methods. 2923 2924 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2925 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2926 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2927 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2928 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2929 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2930 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2931 """ 2932 if isinstance(candles, str): 2933 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2934 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2935 2936 elif isinstance(candles, pd.DataFrame): 2937 self.priceModel.prices = candles # set candles chain from variable 2938 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2939 2940 if "datetime" not in candles.columns: 2941 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2942 2943 else: 2944 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2945 raise Exception("Incorrect value") 2946 2947 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2948 2949 if interact: 2950 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2951 2952 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2953 2954 else: 2955 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2956 2957 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2958 2959 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2960 2961 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2962 """ 2963 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2964 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2965 2966 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2967 2968 :param operation: string "Buy" or "Sell". 2969 :param lots: volume, integer count of lots >= 1. 2970 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2971 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2972 :param expDate: string "Undefined" by default or local date in future, 2973 it is a string with format `%Y-%m-%d %H:%M:%S`. 2974 :return: JSON with response from broker server. 2975 """ 2976 if self.accountId is None or not self.accountId: 2977 uLogger.error("Variable `accountId` must be defined for using this method!") 2978 raise Exception("Account ID required") 2979 2980 if operation is None or not operation or operation not in ("Buy", "Sell"): 2981 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2982 raise Exception("Incorrect value") 2983 2984 if lots is None or lots < 1: 2985 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2986 lots = 1 2987 2988 if tp is None or tp < 0: 2989 tp = 0 2990 2991 if sl is None or sl < 0: 2992 sl = 0 2993 2994 if expDate is None or not expDate: 2995 expDate = "Undefined" 2996 2997 if not (self._ticker or self._figi): 2998 uLogger.error("Ticker or FIGI must be defined!") 2999 raise Exception("Ticker or FIGI required") 3000 3001 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3002 self._ticker = instrument["ticker"] 3003 self._figi = instrument["figi"] 3004 3005 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 3006 3007 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3008 self.body = str({ 3009 "figi": self._figi, 3010 "quantity": str(lots), 3011 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3012 "accountId": str(self.accountId), 3013 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3014 }) 3015 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3016 3017 if "orderId" in response.keys(): 3018 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3019 operation, response["orderId"], 3020 self._ticker, self._figi, lots, 3021 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3022 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3023 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3024 )) 3025 3026 if tp > 0: 3027 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3028 3029 if sl > 0: 3030 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3031 3032 else: 3033 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3034 3035 return response 3036 3037 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3038 """ 3039 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3040 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3041 3042 See also: `Order()` and `Trade()` docstrings. 3043 3044 :param lots: volume, integer count of lots >= 1. 3045 :param tp: float > 0, take profit price of stop-order. 3046 :param sl: float > 0, stop loss price of stop-order. 3047 :param expDate: it's a local date in future. 3048 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3049 :return: JSON with response from broker server. 3050 """ 3051 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3052 3053 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3054 """ 3055 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3056 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3057 3058 See also: `Order()` and `Trade()` docstrings. 3059 3060 :param lots: volume, integer count of lots >= 1. 3061 :param tp: float > 0, take profit price of stop-order. 3062 :param sl: float > 0, stop loss price of stop-order. 3063 :param expDate: it's a local date in the future. 3064 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3065 :return: JSON with response from broker server. 3066 """ 3067 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3068 3069 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3070 """ 3071 Close position of given instruments. 3072 3073 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3074 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3075 This avoids unnecessary downloading data from the server. 3076 """ 3077 if instruments is None or not instruments: 3078 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3079 raise Exception("Ticker or FIGI required") 3080 3081 if isinstance(instruments, str): 3082 instruments = [instruments] 3083 3084 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3085 if uniqueInstruments: 3086 if portfolio is None or not portfolio: 3087 portfolio = self.Overview(show=False) 3088 3089 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3090 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3091 3092 for self._figi in uniqueInstruments: 3093 if self._figi not in allOpened: 3094 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3095 continue 3096 3097 # search open trade info about instrument by ticker: 3098 instrument = {} 3099 for iType in TKS_INSTRUMENTS: 3100 if instrument: 3101 break 3102 3103 for item in portfolio["stat"][iType]: 3104 if item["figi"] == self._figi: 3105 instrument = item 3106 break 3107 3108 if instrument: 3109 self._ticker = instrument["ticker"] 3110 self._figi = instrument["figi"] 3111 3112 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3113 self._ticker, 3114 self._figi, 3115 int(instrument["volume"]), 3116 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3117 )) 3118 3119 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3120 3121 if tradeLots > 0: 3122 if instrument["blocked"] > 0: 3123 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3124 instrument["blocked"], 3125 self._ticker, 3126 tradeLots, 3127 )) 3128 3129 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3130 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3131 3132 else: 3133 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3134 3135 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3136 """ 3137 Close all positions of given instruments with defined type. 3138 3139 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3140 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3141 This avoids unnecessary downloading data from the server. 3142 """ 3143 if iType not in TKS_INSTRUMENTS: 3144 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3145 3146 else: 3147 if portfolio is None or not portfolio: 3148 portfolio = self.Overview(show=False) 3149 3150 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3151 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3152 3153 if tickers and portfolio: 3154 self.CloseTrades(tickers, portfolio) 3155 3156 else: 3157 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3158 3159 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3160 """ 3161 Universal method to create market or limit orders with all available parameters for current `accountId`. 3162 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3163 3164 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3165 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3166 3167 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3168 then broker immediately open market order as you can do simple --buy or --sell operations! 3169 3170 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3171 When current price will go up or down to target price value then broker opens a limit order. 3172 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3173 3174 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3175 3176 :param operation: string "Buy" or "Sell". 3177 :param orderType: string "Limit" or "Stop". 3178 :param lots: volume, integer count of lots >= 1. 3179 :param targetPrice: target price > 0. This is open trade price for limit order. 3180 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3181 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3182 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3183 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3184 Stop loss order always executed by market price. 3185 :param expDate: string "Undefined" by default or local date in future. 3186 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3187 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3188 A limit order has no expiration date, it lasts until the end of the trading day. 3189 :return: JSON with response from broker server. 3190 """ 3191 if self.accountId is None or not self.accountId: 3192 uLogger.error("Variable `accountId` must be defined for using this method!") 3193 raise Exception("Account ID required") 3194 3195 if operation is None or not operation or operation not in ("Buy", "Sell"): 3196 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3197 raise Exception("Incorrect value") 3198 3199 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3200 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3201 raise Exception("Incorrect value") 3202 3203 if lots is None or lots < 1: 3204 uLogger.error("You must define trade volume > 0: integer count of lots!") 3205 raise Exception("Incorrect value") 3206 3207 if targetPrice is None or targetPrice <= 0: 3208 uLogger.error("Target price for limit-order must be greater than 0!") 3209 raise Exception("Incorrect value") 3210 3211 if limitPrice is None or limitPrice <= 0: 3212 limitPrice = targetPrice 3213 3214 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3215 stopType = "Limit" 3216 3217 if expDate is None or not expDate: 3218 expDate = "Undefined" 3219 3220 if not (self._ticker or self._figi): 3221 uLogger.error("Tocker or FIGI must be defined!") 3222 raise Exception("Ticker or FIGI required") 3223 3224 response = {} 3225 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3226 self._ticker = instrument["ticker"] 3227 self._figi = instrument["figi"] 3228 3229 if orderType == "Limit": 3230 uLogger.debug( 3231 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3232 self._ticker, self._figi, 3233 operation, lots, targetPrice, instrument["currency"], 3234 )) 3235 3236 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3237 self.body = str({ 3238 "figi": self._figi, 3239 "quantity": str(lots), 3240 "price": FloatToNano(targetPrice), 3241 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3242 "accountId": str(self.accountId), 3243 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3244 }) 3245 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3246 3247 if "orderId" in response.keys(): 3248 uLogger.info( 3249 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3250 response["orderId"], self._ticker, self._figi, operation, lots, 3251 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3252 )) 3253 3254 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3255 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3256 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3257 targetPrice, instrument["currency"], 3258 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3259 )) 3260 3261 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3262 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3263 targetPrice, instrument["currency"], 3264 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3265 )) 3266 3267 else: 3268 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3269 3270 if orderType == "Stop": 3271 uLogger.debug( 3272 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3273 self._ticker, self._figi, 3274 operation, lots, 3275 targetPrice, instrument["currency"], 3276 limitPrice, instrument["currency"], 3277 stopType, expDate, 3278 )) 3279 3280 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3281 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3282 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3283 3284 body = { 3285 "figi": self._figi, 3286 "quantity": str(lots), 3287 "price": FloatToNano(limitPrice), 3288 "stopPrice": FloatToNano(targetPrice), 3289 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3290 "accountId": str(self.accountId), 3291 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3292 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3293 } 3294 3295 if expDateUTC: 3296 body["expireDate"] = expDateUTC 3297 3298 self.body = str(body) 3299 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3300 3301 if "stopOrderId" in response.keys(): 3302 uLogger.info( 3303 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3304 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3305 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3306 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3307 TKS_STOP_ORDER_TYPES[stopOrderType], 3308 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3309 )) 3310 3311 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3312 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3313 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3314 targetPrice, instrument["currency"], 3315 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3316 )) 3317 3318 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3319 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3320 targetPrice, instrument["currency"], 3321 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3322 )) 3323 3324 else: 3325 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3326 3327 return response 3328 3329 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3330 """ 3331 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3332 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3333 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3334 See also: `Order()` docstring. 3335 3336 :param lots: volume, integer count of lots >= 1. 3337 :param targetPrice: target price > 0. This is open trade price for limit order. 3338 :return: JSON with response from broker server. 3339 """ 3340 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3341 3342 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3343 """ 3344 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3345 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3346 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3347 target price value then broker opens a limit order. See also: `Order()` docstring. 3348 3349 :param lots: volume, integer count of lots >= 1. 3350 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3351 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3352 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3353 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3354 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3355 :param expDate: string "Undefined" by default or local date in future. 3356 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3357 This date is converting to UTC format for server. 3358 :return: JSON with response from broker server. 3359 """ 3360 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3361 3362 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3363 """ 3364 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3365 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3366 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3367 See also: `Order()` docstring. 3368 3369 :param lots: volume, integer count of lots >= 1. 3370 :param targetPrice: target price > 0. This is open trade price for limit order. 3371 :return: JSON with response from broker server. 3372 """ 3373 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3374 3375 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3376 """ 3377 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3378 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3379 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3380 target price value then broker opens a limit order. See also: `Order()` docstring. 3381 3382 :param lots: volume, integer count of lots >= 1. 3383 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3384 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3385 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3386 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3387 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3388 :param expDate: string "Undefined" by default or local date in future. 3389 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3390 This date is converting to UTC format for server. 3391 :return: JSON with response from broker server. 3392 """ 3393 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3394 3395 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3396 """ 3397 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3398 3399 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3400 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3401 This avoids unnecessary downloading data from the server. 3402 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3403 """ 3404 if self.accountId is None or not self.accountId: 3405 uLogger.error("Variable `accountId` must be defined for using this method!") 3406 raise Exception("Account ID required") 3407 3408 if orderIDs: 3409 if allOrdersIDs is None: 3410 rawOrders = self.RequestPendingOrders() 3411 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3412 3413 if allStopOrdersIDs is None: 3414 rawStopOrders = self.RequestStopOrders() 3415 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3416 3417 for orderID in orderIDs: 3418 idInPendingOrders = orderID in allOrdersIDs 3419 idInStopOrders = orderID in allStopOrdersIDs 3420 3421 if not (idInPendingOrders or idInStopOrders): 3422 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3423 continue 3424 3425 else: 3426 if idInPendingOrders: 3427 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3428 3429 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3430 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3431 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3432 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3433 3434 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3435 if self.moreDebug: 3436 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3437 3438 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3439 3440 else: 3441 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3442 3443 elif idInStopOrders: 3444 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3445 3446 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3447 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3448 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3449 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3450 3451 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3452 if self.moreDebug: 3453 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3454 3455 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3456 3457 else: 3458 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3459 3460 else: 3461 continue 3462 3463 def CloseAllOrders(self) -> None: 3464 """ 3465 Gets a list of open pending and stop orders and cancel it all. 3466 """ 3467 rawOrders = self.RequestPendingOrders() 3468 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3469 lenOrders = len(allOrdersIDs) 3470 3471 rawStopOrders = self.RequestStopOrders() 3472 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3473 lenSOrders = len(allStopOrdersIDs) 3474 3475 if lenOrders > 0 or lenSOrders > 0: 3476 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3477 3478 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3479 3480 else: 3481 uLogger.info("Orders not found, nothing to cancel.") 3482 3483 def CloseAll(self, *args) -> None: 3484 """ 3485 Close all available (not blocked) opened trades and orders. 3486 3487 Also, you can select one or more keywords case-insensitive: 3488 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3489 3490 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3491 """ 3492 overview = self.Overview(show=False) # get all open trades info 3493 3494 if len(args) == 0: 3495 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3496 self.CloseAllOrders() # close all pending and stop orders 3497 3498 for iType in TKS_INSTRUMENTS: 3499 if iType != "Currencies": 3500 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3501 3502 else: 3503 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3504 lowerArgs = [x.lower() for x in args] 3505 3506 if "orders" in lowerArgs: 3507 self.CloseAllOrders() # close all pending and stop orders 3508 3509 for iType in TKS_INSTRUMENTS: 3510 if iType.lower() in lowerArgs and iType != "Currencies": 3511 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3512 3513 def CloseAllByTicker(self, instrument: str) -> None: 3514 """ 3515 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3516 3517 This method searches opened trade and orders of instrument throw all portfolio and then use 3518 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3519 3520 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3521 3522 :param instrument: string with ticker. 3523 """ 3524 if instrument is None or not instrument: 3525 uLogger.error("Ticker name must be defined for using this method!") 3526 raise Exception("Ticker required") 3527 3528 overview = self.Overview(show=False) # get user portfolio with all open trades info 3529 3530 self._ticker = instrument # try to set instrument as ticker 3531 self._figi = "" 3532 3533 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3534 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3535 3536 if limitAll and self.IsInLimitOrders(portfolio=overview): 3537 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3538 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3539 3540 if stopAll and self.IsInStopOrders(portfolio=overview): 3541 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3542 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3543 3544 if self.IsInPortfolio(portfolio=overview): 3545 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3546 self.CloseTrades(instruments=[instrument], portfolio=overview) 3547 3548 def CloseAllByFIGI(self, instrument: str) -> None: 3549 """ 3550 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3551 3552 This method searches opened trade and orders of instrument throw all portfolio and then use 3553 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3554 3555 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3556 3557 :param instrument: string with FIGI id. 3558 """ 3559 if instrument is None or not instrument: 3560 uLogger.error("FIGI id must be defined for using this method!") 3561 raise Exception("FIGI required") 3562 3563 overview = self.Overview(show=False) # get user portfolio with all open trades info 3564 3565 self._ticker = "" 3566 self._figi = instrument # try to set instrument as FIGI id 3567 3568 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3569 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3570 3571 if limitAll and self.IsInLimitOrders(portfolio=overview): 3572 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3573 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3574 3575 if stopAll and self.IsInStopOrders(portfolio=overview): 3576 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3577 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3578 3579 if self.IsInPortfolio(portfolio=overview): 3580 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3581 self.CloseTrades(instruments=[instrument], portfolio=overview) 3582 3583 @staticmethod 3584 def ParseOrderParameters(operation, **inputParameters): 3585 """ 3586 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3587 3588 :param operation: string "Buy" or "Sell". 3589 :param inputParameters: this is dict of strings that looks like this 3590 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3591 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3592 "prices" key: one or more prices to open limit-orders 3593 Counts of values in lots and prices lists must be equals! 3594 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3595 """ 3596 # TODO: update order grid work with api v2 3597 pass 3598 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3599 # 3600 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3601 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3602 # raise Exception("Incorrect value") 3603 # 3604 # if "l" in inputParameters.keys(): 3605 # inputParameters["lots"] = inputParameters.pop("l") 3606 # 3607 # if "p" in inputParameters.keys(): 3608 # inputParameters["prices"] = inputParameters.pop("p") 3609 # 3610 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3611 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3612 # raise Exception("Incorrect value") 3613 # 3614 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3615 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3616 # 3617 # if len(lots) != len(prices): 3618 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3619 # raise Exception("Incorrect value") 3620 # 3621 # uLogger.debug("Extracted parameters for orders:") 3622 # uLogger.debug("lots = {}".format(lots)) 3623 # uLogger.debug("prices = {}".format(prices)) 3624 # 3625 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3626 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3627 # uLogger.debug("Order parameters: {}".format(result)) 3628 # 3629 # return result 3630 3631 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3632 """ 3633 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3634 3635 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3636 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3637 """ 3638 result = False 3639 msg = "Instrument not defined!" 3640 3641 if portfolio is None or not portfolio: 3642 portfolio = self.Overview(show=False) 3643 3644 if self._ticker: 3645 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3646 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3647 3648 for iType in TKS_INSTRUMENTS: 3649 for instrument in portfolio["stat"][iType]: 3650 if instrument["ticker"] == self._ticker: 3651 result = True 3652 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3653 break 3654 3655 elif self._figi: 3656 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3657 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3658 3659 for iType in TKS_INSTRUMENTS: 3660 for instrument in portfolio["stat"][iType]: 3661 if instrument["figi"] == self._figi: 3662 result = True 3663 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3664 break 3665 3666 else: 3667 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3668 3669 uLogger.debug(msg) 3670 3671 return result 3672 3673 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3674 """ 3675 Returns instrument from the user's portfolio if it presents there. 3676 Instrument must be defined by `ticker` (highly priority) or `figi`. 3677 3678 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3679 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3680 """ 3681 result = None 3682 msg = "Instrument not defined!" 3683 3684 if portfolio is None or not portfolio: 3685 portfolio = self.Overview(show=False) 3686 3687 if self._ticker: 3688 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3689 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3690 3691 for iType in TKS_INSTRUMENTS: 3692 for instrument in portfolio["stat"][iType]: 3693 if instrument["ticker"] == self._ticker: 3694 result = instrument 3695 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3696 break 3697 3698 elif self._figi: 3699 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3700 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3701 3702 for iType in TKS_INSTRUMENTS: 3703 for instrument in portfolio["stat"][iType]: 3704 if instrument["figi"] == self._figi: 3705 result = instrument 3706 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3707 break 3708 3709 else: 3710 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3711 3712 uLogger.debug(msg) 3713 3714 return result 3715 3716 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3717 """ 3718 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3719 3720 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3721 3722 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3723 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3724 """ 3725 result = False 3726 msg = "Instrument not defined!" 3727 3728 if portfolio is None or not portfolio: 3729 portfolio = self.Overview(show=False) 3730 3731 if self._ticker: 3732 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3733 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3734 3735 for instrument in portfolio["stat"]["orders"]: 3736 if instrument["ticker"] == self._ticker: 3737 result = True 3738 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3739 break 3740 3741 elif self._figi: 3742 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3743 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3744 3745 for instrument in portfolio["stat"]["orders"]: 3746 if instrument["figi"] == self._figi: 3747 result = True 3748 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3749 break 3750 3751 else: 3752 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3753 3754 uLogger.debug(msg) 3755 3756 return result 3757 3758 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3759 """ 3760 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3761 Instrument must be defined by `ticker` (highly priority) or `figi`. 3762 3763 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3764 3765 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3766 :return: list with `orderID`s of limit orders. 3767 """ 3768 result = [] 3769 msg = "Instrument not defined!" 3770 3771 if portfolio is None or not portfolio: 3772 portfolio = self.Overview(show=False) 3773 3774 if self._ticker: 3775 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3776 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3777 3778 for instrument in portfolio["stat"]["orders"]: 3779 if instrument["ticker"] == self._ticker: 3780 result.append(instrument["orderID"]) 3781 3782 if result: 3783 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3784 3785 elif self._figi: 3786 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3787 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3788 3789 for instrument in portfolio["stat"]["orders"]: 3790 if instrument["figi"] == self._figi: 3791 result.append(instrument["orderID"]) 3792 3793 if result: 3794 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3795 3796 else: 3797 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3798 3799 uLogger.debug(msg) 3800 3801 return result 3802 3803 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3804 """ 3805 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3806 3807 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3808 3809 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3810 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3811 """ 3812 result = False 3813 msg = "Instrument not defined!" 3814 3815 if portfolio is None or not portfolio: 3816 portfolio = self.Overview(show=False) 3817 3818 if self._ticker: 3819 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3820 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3821 3822 for instrument in portfolio["stat"]["stopOrders"]: 3823 if instrument["ticker"] == self._ticker: 3824 result = True 3825 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3826 break 3827 3828 elif self._figi: 3829 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3830 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3831 3832 for instrument in portfolio["stat"]["stopOrders"]: 3833 if instrument["figi"] == self._figi: 3834 result = True 3835 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3836 break 3837 3838 else: 3839 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3840 3841 uLogger.debug(msg) 3842 3843 return result 3844 3845 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3846 """ 3847 Returns list with all `orderID`s of opened stop orders for the instrument. 3848 Instrument must be defined by `ticker` (highly priority) or `figi`. 3849 3850 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3851 3852 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3853 :return: list with `orderID`s of stop orders. 3854 """ 3855 result = [] 3856 msg = "Instrument not defined!" 3857 3858 if portfolio is None or not portfolio: 3859 portfolio = self.Overview(show=False) 3860 3861 if self._ticker: 3862 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3863 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3864 3865 for instrument in portfolio["stat"]["stopOrders"]: 3866 if instrument["ticker"] == self._ticker: 3867 result.append(instrument["orderID"]) 3868 3869 if result: 3870 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3871 3872 elif self._figi: 3873 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3874 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3875 3876 for instrument in portfolio["stat"]["stopOrders"]: 3877 if instrument["figi"] == self._figi: 3878 result.append(instrument["orderID"]) 3879 3880 if result: 3881 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3882 3883 else: 3884 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3885 3886 uLogger.debug(msg) 3887 3888 return result 3889 3890 def RequestLimits(self) -> dict: 3891 """ 3892 Method for obtaining the available funds for withdrawal for current `accountId`. 3893 3894 See also: 3895 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3896 - `OverviewLimits()` method 3897 3898 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3899 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3900 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3901 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3902 """ 3903 if self.accountId is None or not self.accountId: 3904 uLogger.error("Variable `accountId` must be defined for using this method!") 3905 raise Exception("Account ID required") 3906 3907 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3908 3909 self.body = str({"accountId": self.accountId}) 3910 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3911 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3912 3913 if self.moreDebug: 3914 uLogger.debug("Records about available funds for withdrawal successfully received") 3915 3916 return rawLimits 3917 3918 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3919 """ 3920 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3921 3922 See also: `RequestLimits()`. 3923 3924 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3925 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3926 :return: dict with raw parsed data from server and some calculated statistics about it. 3927 """ 3928 if self.accountId is None or not self.accountId: 3929 uLogger.error("Variable `accountId` must be defined for using this method!") 3930 raise Exception("Account ID required") 3931 3932 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3933 3934 view = { 3935 "rawLimits": rawLimits, 3936 "limits": { # parsed data for every currency: 3937 "money": { # this is an array of portfolio currency positions 3938 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3939 }, 3940 "blocked": { # this is an array of blocked currency 3941 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3942 }, 3943 "blockedGuarantee": { # this is locked money under collateral for futures 3944 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3945 }, 3946 }, 3947 } 3948 3949 # --- Prepare text table with limits in human-readable format: 3950 if show or onlyFiles: 3951 info = [ 3952 "# Withdrawal limits\n\n", 3953 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3954 "* **Account ID:** [{}]\n".format(self.accountId), 3955 ] 3956 3957 if view["limits"]["money"]: 3958 info.extend([ 3959 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3960 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3961 ]) 3962 3963 else: 3964 info.append("\nNo withdrawal limits\n") 3965 3966 for curr in view["limits"]["money"].keys(): 3967 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3968 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3969 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3970 3971 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3972 "[{}]".format(curr), 3973 "{:.2f}".format(view["limits"]["money"][curr]), 3974 "{:.2f}".format(availableMoney), 3975 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3976 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3977 ) 3978 3979 if curr == "rub": 3980 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3981 3982 else: 3983 info.append(infoStr) 3984 3985 infoText = "".join(info) 3986 3987 if show and not onlyFiles: 3988 uLogger.info(infoText) 3989 3990 if self.withdrawalLimitsFile and (show or onlyFiles): 3991 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3992 fH.write(infoText) 3993 3994 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3995 3996 if self.useHTMLReports: 3997 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3998 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3999 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 4000 4001 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4002 4003 return view 4004 4005 def RequestAccounts(self) -> dict: 4006 """ 4007 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4008 4009 See also: 4010 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4011 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4012 - `OverviewUserInfo()` method 4013 4014 :return: dict with raw data from server that contains accounts info. Example of dict: 4015 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4016 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4017 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4018 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4019 """ 4020 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4021 4022 self.body = str({}) 4023 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4024 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4025 4026 if self.moreDebug: 4027 uLogger.debug("Records about available accounts successfully received") 4028 4029 return rawAccounts 4030 4031 def RequestUserInfo(self) -> dict: 4032 """ 4033 Method for requesting common user's information. 4034 4035 See also: 4036 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4037 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4038 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4039 - `OverviewUserInfo()` method 4040 4041 :return: dict with raw data from server that contains user's information. Example of dict: 4042 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4043 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4044 """ 4045 uLogger.debug("Requesting common user's information. Wait, please...") 4046 4047 self.body = str({}) 4048 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4049 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4050 4051 if self.moreDebug: 4052 uLogger.debug("Records about current user successfully received") 4053 4054 return rawUserInfo 4055 4056 def RequestMarginStatus(self, accountId: str = None) -> dict: 4057 """ 4058 Method for requesting margin calculation for defined account ID. 4059 4060 See also: 4061 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4062 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4063 - `OverviewUserInfo()` method 4064 4065 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4066 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4067 Example of responses: 4068 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4069 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4070 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4071 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4072 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4073 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4074 """ 4075 if accountId is None or not accountId: 4076 if self.accountId is None or not self.accountId: 4077 uLogger.error("Variable `accountId` must be defined for using this method!") 4078 raise Exception("Account ID required") 4079 4080 else: 4081 accountId = self.accountId # use `self.accountId` (main ID) by default 4082 4083 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4084 4085 self.body = str({"accountId": accountId}) 4086 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4087 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4088 4089 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4090 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4091 rawMargin = {} 4092 4093 else: 4094 if self.moreDebug: 4095 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4096 4097 return rawMargin 4098 4099 def RequestTariffLimits(self) -> dict: 4100 """ 4101 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4102 4103 See also: 4104 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4105 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4106 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4107 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4108 - `OverviewUserInfo()` method 4109 4110 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4111 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4112 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4113 """ 4114 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4115 4116 self.body = str({}) 4117 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4118 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4119 4120 if self.moreDebug: 4121 uLogger.debug("Records with limits of current tariff successfully received") 4122 4123 return rawTariffLimits 4124 4125 def RequestBondCoupons(self, iJSON: dict) -> dict: 4126 """ 4127 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4128 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4129 All dates are in UTC timezone. 4130 4131 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4132 Documentation: 4133 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4134 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4135 4136 See also: `ExtendBondsData()`. 4137 4138 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4139 If raw iJSON is not data of bond then server returns an error [400] with message: 4140 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4141 :return: dictionary with bond payment calendar. Response example 4142 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4143 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4144 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4145 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4146 """ 4147 if iJSON["figi"] is None or not iJSON["figi"]: 4148 uLogger.error("FIGI must be defined for using this method!") 4149 raise Exception("FIGI required") 4150 4151 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4152 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4153 4154 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4155 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4156 self._figi, 4157 startDate, 4158 endDate, 4159 )) 4160 4161 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4162 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4163 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4164 4165 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4166 uLogger.warning("Instrument type is not bond!") 4167 4168 else: 4169 if self.moreDebug: 4170 uLogger.debug("Records about bond payment calendar successfully received") 4171 4172 return calendar 4173 4174 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4175 """ 4176 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4177 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4178 coupon yields, current yields and some statistics etc. 4179 4180 WARNING! This is too long operation if a lot of bonds requested from broker server. 4181 4182 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4183 4184 :param instruments: list of strings with tickers or FIGIs. 4185 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4186 for further used by data scientists or stock analytics. 4187 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4188 In XLSX-file and Pandas DataFrame fields mean: 4189 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4190 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4191 """ 4192 if instruments is None or not instruments: 4193 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4194 raise Exception("Ticker or FIGI required") 4195 4196 if isinstance(instruments, str): 4197 instruments = [instruments] 4198 4199 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4200 4201 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4202 4203 iCount = len(uniqueInstruments) 4204 tooLong = iCount >= 20 4205 if tooLong: 4206 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4207 4208 bonds = None 4209 for i, self._figi in enumerate(uniqueInstruments): 4210 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4211 4212 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4213 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4214 rawBond = self.SearchByFIGI(requestPrice=True) 4215 4216 # Widen raw data with UTC current time (iData["actualDateTime"]): 4217 actualDate = datetime.now(tzutc()) 4218 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4219 4220 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4221 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4222 4223 # Replace some values with human-readable: 4224 iData["nominalCurrency"] = iData["nominal"]["currency"] 4225 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4226 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4227 iData["aciCurrency"] = iData["aciValue"]["currency"] 4228 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4229 iData["issueSize"] = int(iData["issueSize"]) 4230 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4231 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4232 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4233 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4234 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4235 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4236 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4237 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4238 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4239 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4240 4241 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4242 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4243 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4244 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4245 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4246 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4247 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4248 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4249 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4250 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4251 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4252 4253 # Widen raw data with calendar data from `rawCalendar` values: 4254 calendarData = [] 4255 if "events" in iData["rawCalendar"].keys(): 4256 for item in iData["rawCalendar"]["events"]: 4257 calendarData.append({ 4258 "couponDate": item["couponDate"], 4259 "couponNumber": int(item["couponNumber"]), 4260 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4261 "payCurrency": item["payOneBond"]["currency"], 4262 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4263 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4264 "couponStartDate": item["couponStartDate"], 4265 "couponEndDate": item["couponEndDate"], 4266 "couponPeriod": item["couponPeriod"], 4267 }) 4268 4269 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4270 if "maturityDate" not in iData.keys(): 4271 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4272 4273 # Widen raw data with Coupon Rate. 4274 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4275 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4276 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4277 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4278 4279 # Widen raw data with Yield to Maturity (YTM) on current date. 4280 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4281 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4282 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4283 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4284 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4285 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4286 4287 iData["calendar"] = calendarData # adds calendar at the end 4288 4289 # Remove not used data: 4290 iData.pop("uid") 4291 iData.pop("positionUid") 4292 iData.pop("currentPrice") 4293 iData.pop("rawCalendar") 4294 4295 colNames = list(iData.keys()) 4296 if bonds is None: 4297 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4298 4299 else: 4300 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4301 4302 else: 4303 uLogger.warning("Instrument is not a bond!") 4304 4305 processed = round(100 * (i + 1) / iCount, 1) 4306 if tooLong and processed % 5 == 0: 4307 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4308 4309 else: 4310 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4311 4312 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4313 4314 # Saving bonds from Pandas DataFrame to XLSX sheet: 4315 if xlsx and self.bondsXLSXFile: 4316 with pd.ExcelWriter( 4317 path=self.bondsXLSXFile, 4318 date_format=TKS_DATE_FORMAT, 4319 datetime_format=TKS_DATE_TIME_FORMAT, 4320 mode="w", 4321 ) as writer: 4322 bonds.to_excel( 4323 writer, 4324 sheet_name="Extended bonds data", 4325 index=True, 4326 encoding="UTF-8", 4327 freeze_panes=(1, 1), 4328 ) # saving as XLSX-file with freeze first row and column as headers 4329 4330 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4331 4332 return bonds 4333 4334 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4335 """ 4336 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4337 4338 WARNING! This is too long operation if a lot of bonds requested from broker server. 4339 4340 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4341 4342 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4343 extended information about bonds: main info, current prices, bond payment calendar, 4344 coupon yields, current yields and some statistics etc. 4345 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4346 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4347 for further used by data scientists or stock analytics. 4348 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4349 """ 4350 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4351 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4352 4353 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4354 4355 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4356 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4357 calendar = None 4358 for bond in extBonds.iterrows(): 4359 for item in bond[1]["calendar"]: 4360 cData = { 4361 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4362 "couponDate": item["couponDate"], 4363 "figi": bond[1]["figi"], 4364 "ticker": bond[1]["ticker"], 4365 "name": bond[1]["name"], 4366 "couponNumber": item["couponNumber"], 4367 "payOneBond": item["payOneBond"], 4368 "payCurrency": item["payCurrency"], 4369 "couponType": item["couponType"], 4370 "couponPeriod": item["couponPeriod"], 4371 "fixDate": item["fixDate"], 4372 "couponStartDate": item["couponStartDate"], 4373 "couponEndDate": item["couponEndDate"], 4374 } 4375 4376 if calendar is None: 4377 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4378 4379 else: 4380 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4381 4382 if calendar is not None: 4383 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4384 4385 # Saving calendar from Pandas DataFrame to XLSX sheet: 4386 if xlsx: 4387 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4388 4389 with pd.ExcelWriter( 4390 path=xlsxCalendarFile, 4391 date_format=TKS_DATE_FORMAT, 4392 datetime_format=TKS_DATE_TIME_FORMAT, 4393 mode="w", 4394 ) as writer: 4395 humanReadable = calendar.copy(deep=True) 4396 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4397 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4398 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4399 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4400 humanReadable.columns = colNames # human-readable column names 4401 4402 humanReadable.to_excel( 4403 writer, 4404 sheet_name="Bond payments calendar", 4405 index=False, 4406 encoding="UTF-8", 4407 freeze_panes=(1, 2), 4408 ) # saving as XLSX-file with freeze first row and column as headers 4409 4410 del humanReadable # release df in memory 4411 4412 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4413 4414 return calendar 4415 4416 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4417 """ 4418 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4419 Also, creates Markdown file with calendar data, `calendar.md` by default. 4420 4421 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4422 4423 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4424 extended information about bonds: main info, current prices, bond payment calendar, 4425 coupon yields, current yields and some statistics etc. 4426 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4427 :param show: if `True` then also printing bonds payment calendar to the console, 4428 otherwise save to file `calendarFile` only. `False` by default. 4429 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4430 :return: multilines text in Markdown format with bonds payment calendar as a table. 4431 """ 4432 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4433 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4434 4435 infoText = "# Bond payments calendar\n\n" 4436 4437 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4438 4439 if not (calendar is None or calendar.empty): 4440 splitLine = "| | | | | | | | | |\n" 4441 4442 info = [ 4443 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4444 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4445 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4446 ] 4447 4448 newMonth = False 4449 notOneBond = calendar["figi"].nunique() > 1 4450 for i, bond in enumerate(calendar.iterrows()): 4451 if newMonth and notOneBond: 4452 info.append(splitLine) 4453 4454 info.append( 4455 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4456 " √" if bond[1]["paid"] else " —", 4457 bond[1]["couponDate"].split("T")[0], 4458 bond[1]["figi"], 4459 bond[1]["ticker"], 4460 bond[1]["couponNumber"], 4461 "{} {}".format( 4462 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4463 bond[1]["payCurrency"], 4464 ), 4465 bond[1]["couponType"], 4466 bond[1]["couponPeriod"], 4467 bond[1]["fixDate"].split("T")[0], 4468 ) 4469 ) 4470 4471 if i < len(calendar.values) - 1: 4472 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4473 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4474 newMonth = False if curDate.month == nextDate.month else True 4475 4476 else: 4477 newMonth = False 4478 4479 infoText += "".join(info) 4480 4481 if show and not onlyFiles: 4482 uLogger.info("{}".format(infoText)) 4483 4484 if self.calendarFile is not None and (show or onlyFiles): 4485 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4486 fH.write(infoText) 4487 4488 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4489 4490 if self.useHTMLReports: 4491 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4492 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4493 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4494 4495 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4496 4497 else: 4498 infoText += "No data\n" 4499 4500 return infoText 4501 4502 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4503 """ 4504 Method for parsing and show simple table with all available user accounts. 4505 4506 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4507 4508 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4509 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4510 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4511 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4512 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4513 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4514 "closed": "—", "access": "Full access" }, ...}}` 4515 """ 4516 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4517 4518 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4519 accounts = { 4520 item["id"]: { 4521 "type": TKS_ACCOUNT_TYPES[item["type"]], 4522 "name": item["name"], 4523 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4524 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4525 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4526 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4527 } for item in rawAccounts["accounts"] 4528 } 4529 4530 # Raw and parsed data with some fields replaced in "stat" section: 4531 view = { 4532 "rawAccounts": rawAccounts, 4533 "stat": accounts, 4534 } 4535 4536 # --- Prepare simple text table with only accounts data in human-readable format: 4537 if show or onlyFiles: 4538 info = [ 4539 "# User accounts\n\n", 4540 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4541 "| Account ID | Type | Status | Name |\n", 4542 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4543 ] 4544 4545 for account in view["stat"].keys(): 4546 info.extend([ 4547 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4548 account, 4549 view["stat"][account]["type"], 4550 view["stat"][account]["status"], 4551 view["stat"][account]["name"], 4552 ) 4553 ]) 4554 4555 infoText = "".join(info) 4556 4557 if show and not onlyFiles: 4558 uLogger.info(infoText) 4559 4560 if self.userAccountsFile and (show or onlyFiles): 4561 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4562 fH.write(infoText) 4563 4564 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4565 4566 if self.useHTMLReports: 4567 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4568 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4569 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4570 4571 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4572 4573 return view 4574 4575 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4576 """ 4577 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4578 4579 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4580 4581 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4582 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4583 :return: dict with raw parsed data from server and some calculated statistics about it. 4584 """ 4585 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4586 tmpTicker = self._ticker 4587 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4588 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4589 self._ticker = tmpTicker 4590 4591 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4592 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4593 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4594 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4595 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4596 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4597 4598 # This is dict with parsed common user data: 4599 userInfo = { 4600 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4601 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4602 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4603 "tariff": rawUserInfo["tariff"], 4604 } 4605 4606 # This is an array of dict with parsed margin statuses for every account IDs: 4607 margins = {} 4608 for accountId in accounts.keys(): 4609 if rawMargins[accountId]: 4610 margins[accountId] = { 4611 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4612 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4613 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4614 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4615 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4616 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4617 "missing": missing["volume"], 4618 } 4619 4620 else: 4621 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4622 4623 unary = {} # unary-connection limits 4624 for item in rawTariffLimits["unaryLimits"]: 4625 if item["limitPerMinute"] in unary.keys(): 4626 unary[item["limitPerMinute"]].extend(item["methods"]) 4627 4628 else: 4629 unary[item["limitPerMinute"]] = item["methods"] 4630 4631 stream = {} # stream-connection limits 4632 for item in rawTariffLimits["streamLimits"]: 4633 if item["limit"] in stream.keys(): 4634 stream[item["limit"]].extend(item["streams"]) 4635 4636 else: 4637 stream[item["limit"]] = item["streams"] 4638 4639 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4640 limits = { 4641 "unary": unary, 4642 "stream": stream, 4643 } 4644 4645 # Raw and parsed data as an output result: 4646 view = { 4647 "rawUserInfo": rawUserInfo, 4648 "rawAccounts": rawAccounts, 4649 "rawMargins": rawMargins, 4650 "rawTariffLimits": rawTariffLimits, 4651 "stat": { 4652 "overview": overview, 4653 "userInfo": userInfo, 4654 "accounts": accounts, 4655 "margins": margins, 4656 "limits": limits, 4657 }, 4658 } 4659 4660 # --- Prepare text table with user information in human-readable format: 4661 if show or onlyFiles: 4662 info = [ 4663 "# Full user information\n\n", 4664 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4665 "## Common information\n\n", 4666 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4667 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4668 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4669 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4670 "\n## User accounts\n\n", 4671 ] 4672 4673 for account in view["stat"]["accounts"].keys(): 4674 info.extend([ 4675 "### ID: [{}]\n\n".format(account), 4676 "| Parameters | Values |\n", 4677 "|----------------------|--------------------------------------------------------------|\n", 4678 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4679 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4680 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4681 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4682 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4683 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4684 ]) 4685 4686 if margins[account]: 4687 info.extend([ 4688 "| Margin status: | Enabled |\n", 4689 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4690 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4691 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4692 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4693 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4694 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4695 ]) 4696 4697 else: 4698 info.append("| Margin status: | Disabled |\n\n") 4699 4700 info.extend([ 4701 "\n## Current user tariff limits\n", 4702 "\n### See also\n", 4703 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4704 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4705 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4706 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4707 "\n### Unary limits\n", 4708 ]) 4709 4710 if unary: 4711 for key, values in sorted(unary.items()): 4712 info.append("\n* Max requests per minute: {}\n".format(key)) 4713 4714 for value in values: 4715 info.append(" - {}\n".format(value)) 4716 4717 else: 4718 info.append("\nNot available\n") 4719 4720 info.append("\n### Stream limits\n") 4721 4722 if stream: 4723 for key, values in sorted(stream.items()): 4724 info.append("\n* Max stream connections: {}\n".format(key)) 4725 4726 for value in values: 4727 info.append(" - {}\n".format(value)) 4728 4729 else: 4730 info.append("\nNot available\n") 4731 4732 infoText = "".join(info) 4733 4734 if show and not onlyFiles: 4735 uLogger.info(infoText) 4736 4737 if self.userInfoFile and (show or onlyFiles): 4738 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4739 fH.write(infoText) 4740 4741 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4742 4743 if self.useHTMLReports: 4744 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4745 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4746 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4747 4748 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4749 4750 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self.__lock = Lock() # initialize multiprocessing mutex lock 130 131 self.aliases = TKS_TICKER_ALIASES 132 """Some aliases instead official tickers. 133 134 See also: `TKSEnums.TKS_TICKER_ALIASES` 135 """ 136 137 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 138 139 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 140 141 self._ticker = "" 142 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 143 144 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 145 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 146 147 See also: `SearchByTicker()`, `SearchInstruments()`. 148 """ 149 150 self._figi = "" 151 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 152 153 See also: `SearchByFIGI()`, `SearchInstruments()`. 154 """ 155 156 self.depth = 1 157 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 158 159 See also: `GetCurrentPrices()`. 160 """ 161 162 self.server = r"https://invest-public-api.tinkoff.ru/rest" 163 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 164 165 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 166 """ 167 168 uLogger.debug("Broker API server: {}".format(self.server)) 169 170 self.timeout = 15 171 """Server operations timeout in seconds. Default: `15`. 172 173 See also: `SendAPIRequest()`. 174 """ 175 176 self.headers = { 177 "Content-Type": "application/json", 178 "accept": "application/json", 179 "Authorization": "Bearer {}".format(self.token), 180 "x-app-name": "Tim55667757.TKSBrokerAPI", 181 } 182 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 183 184 See also: `SendAPIRequest()`. 185 """ 186 187 self.body = None 188 """Request body which send to broker server. Default: `None`. 189 190 See also: `SendAPIRequest()`. 191 """ 192 193 self.moreDebug = False 194 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 195 196 self.useHTMLReports = False 197 """ 198 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 199 200 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 201 """ 202 203 self.historyFile = None 204 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 205 206 See also: `History()`. 207 """ 208 209 self.htmlHistoryFile = "index.html" 210 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 211 212 See also: `ShowHistoryChart()`. 213 """ 214 215 self.instrumentsFile = "instruments.md" 216 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 217 218 See also: `ShowInstrumentsInfo()`. 219 """ 220 221 self.searchResultsFile = "search-results.md" 222 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 223 224 See also: `SearchInstruments()`. 225 """ 226 227 self.pricesFile = "prices.md" 228 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 229 230 See also: `GetListOfPrices()`. 231 """ 232 233 self.infoFile = "info.md" 234 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 235 236 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 237 """ 238 239 self.bondsXLSXFile = "ext-bonds.xlsx" 240 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 241 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 242 243 See also: `ExtendBondsData()`. 244 """ 245 246 self.calendarFile = "calendar.md" 247 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 248 249 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 250 251 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 252 """ 253 254 self.overviewFile = "overview.md" 255 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 256 257 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 258 """ 259 260 self.overviewDigestFile = "overview-digest.md" 261 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 262 263 See also: `Overview()` with parameter `details="digest"`. 264 """ 265 266 self.overviewPositionsFile = "overview-positions.md" 267 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 268 269 See also: `Overview()` with parameter `details="positions"`. 270 """ 271 272 self.overviewOrdersFile = "overview-orders.md" 273 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 274 275 See also: `Overview()` with parameter `details="orders"`. 276 """ 277 278 self.overviewAnalyticsFile = "overview-analytics.md" 279 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 280 281 See also: `Overview()` with parameter `details="analytics"`. 282 """ 283 284 self.overviewBondsCalendarFile = "overview-calendar.md" 285 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 286 287 See also: `Overview()` with parameter `details="calendar"`. 288 """ 289 290 self.reportFile = "deals.md" 291 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 292 293 See also: `Deals()`. 294 """ 295 296 self.withdrawalLimitsFile = "limits.md" 297 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 298 299 See also: `OverviewLimits()` and `RequestLimits()`. 300 """ 301 302 self.userInfoFile = "user-info.md" 303 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 304 305 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 306 """ 307 308 self.userAccountsFile = "accounts.md" 309 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 310 311 See also: `OverviewAccounts()`, `RequestAccounts()`. 312 """ 313 314 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 315 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 316 317 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 318 319 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 320 """ 321 322 self.iList = None # init iList for raw instruments data 323 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 324 325 See also: `Listing()`, `DumpInstruments()`. 326 """ 327 328 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 329 if useCache: 330 if os.path.exists(self.iListDumpFile): 331 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 332 curTime = datetime.now(tzutc()) 333 334 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 335 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 336 337 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 338 339 else: 340 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 341 342 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 343 os.path.abspath(self.iListDumpFile), 344 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 345 )) 346 347 else: 348 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 349 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 350 351 else: 352 self.iList = self.Listing() # request new raw instruments data from broker server 353 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 354 355 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 356 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 357 358 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 359 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.
See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.
See also: Overview() with parameter details="calendar".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.
Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc.
More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.
See also: SearchByFIGI(), SearchInstruments().
419 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 420 """ 421 Send GET or POST request to broker server and receive JSON object. 422 423 self.header: must be defining with dictionary of headers. 424 self.body: if define then used as request body. None by default. 425 self.timeout: global request timeout, 15 seconds by default. 426 :param url: url with REST request. 427 :param reqType: send "GET" or "POST" request. "GET" by default. 428 :param retry: how many times retry after first request if an 5xx server errors occurred. 429 :param pause: sleep time in seconds between retries. 430 :return: response JSON (dictionary) from broker. 431 """ 432 if reqType.upper() not in ("GET", "POST"): 433 uLogger.error("You can define request type: `GET` or `POST`!") 434 raise Exception("Incorrect value") 435 436 if self.moreDebug: 437 uLogger.debug("Request parameters:") 438 uLogger.debug(" - REST API URL: {}".format(url)) 439 uLogger.debug(" - request type: {}".format(reqType)) 440 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 441 uLogger.debug(" - body:\n{}".format(self.body)) 442 443 # fast hack to avoid all operations with some tickers/FIGI 444 responseJSON = {} 445 oK = True 446 for item in self.exclude: 447 if item in url: 448 if self.moreDebug: 449 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 450 451 oK = False 452 break 453 454 if oK: 455 with self.__lock: # acquire the mutex lock 456 counter = 0 457 response = None 458 errMsg = "" 459 460 while not response and counter <= retry: 461 if reqType == "GET": 462 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 463 464 if reqType == "POST": 465 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 466 467 if self.moreDebug: 468 uLogger.debug("Response:") 469 uLogger.debug(" - status code: {}".format(response.status_code)) 470 uLogger.debug(" - reason: {}".format(response.reason)) 471 uLogger.debug(" - body length: {}".format(len(response.text))) 472 uLogger.debug(" - headers:\n{}".format(response.headers)) 473 474 # Server returns some headers: 475 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 476 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 477 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 478 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 479 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 480 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 481 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 482 sleep(rateLimitWait) 483 484 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 485 if 400 <= response.status_code < 500: 486 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 487 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 488 489 if "code" in response.text and "message" in response.text: 490 msgDict = self._ParseJSON(rawData=response.text) 491 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 492 493 counter = retry + 1 # do not retry for 4xx errors 494 495 if 500 <= response.status_code < 600: 496 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 497 uLogger.debug(" - not oK, {}".format(errMsg)) 498 499 if "code" in response.text and "message" in response.text: 500 errMsgDict = self._ParseJSON(rawData=response.text) 501 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 502 503 counter += 1 504 505 if counter <= retry: 506 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 507 sleep(pause) 508 509 responseJSON = self._ParseJSON(rawData=response.text) 510 511 if errMsg: 512 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 513 uLogger.error(" - not oK, {}".format(errMsg)) 514 515 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
548 def Listing(self) -> dict: 549 """ 550 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 551 552 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 553 """ 554 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 555 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 556 557 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 558 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 559 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 560 561 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 562 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 563 poolUpdater.close() # close the thread pool 564 poolUpdater.join() # wait a moment until all data returns from threads 565 566 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 567 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 568 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 569 570 # calculate minimum price increment (step) for all instruments and set up instrument's type: 571 for iType in iList.keys(): 572 for ticker in iList[iType]: 573 iList[iType][ticker]["type"] = iType 574 575 if "minPriceIncrement" in iList[iType][ticker].keys(): 576 iList[iType][ticker]["step"] = NanoToFloat( 577 iList[iType][ticker]["minPriceIncrement"]["units"], 578 iList[iType][ticker]["minPriceIncrement"]["nano"], 579 ) 580 581 else: 582 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 583 584 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
586 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 587 """ 588 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 589 590 See also: `DumpInstruments()`, `Listing()`. 591 592 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 593 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 594 """ 595 if self.iListDumpFile is None or not self.iListDumpFile: 596 uLogger.error("Output name of dump file must be defined!") 597 raise Exception("Filename required") 598 599 if not self.iList or forceUpdate: 600 self.iList = self.Listing() 601 602 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 603 604 # Save as XLSX with separated sheets for every type of instruments: 605 with pd.ExcelWriter( 606 path=xlsxDumpFile, 607 date_format=TKS_DATE_FORMAT, 608 datetime_format=TKS_DATE_TIME_FORMAT, 609 mode="w", 610 ) as writer: 611 for iType in TKS_INSTRUMENTS: 612 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 613 df = df[sorted(df)] # sorted by column names 614 df = df.applymap( 615 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 616 na_action="ignore", 617 ) # converting numbers from nano-type to float in every cell 618 df.to_excel( 619 writer, 620 sheet_name=iType, 621 encoding="UTF-8", 622 freeze_panes=(1, 1), 623 ) # saving as XLSX-file with freeze first row and column as headers 624 625 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
627 def DumpInstruments(self, forceUpdate: bool = True) -> str: 628 """ 629 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 630 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 631 632 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 633 634 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 635 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 636 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 637 """ 638 if self.iListDumpFile is None or not self.iListDumpFile: 639 uLogger.error("Output name of dump file must be defined!") 640 raise Exception("Filename required") 641 642 if not self.iList or forceUpdate: 643 self.iList = self.Listing() 644 645 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 646 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 647 fH.write(jsonDump) 648 649 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 650 651 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
653 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 654 """ 655 Show information about one instrument defined by json data and prints it in Markdown format. 656 657 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 658 659 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 660 :param show: if `True` then also printing information about instrument and its current price. 661 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 662 :return: multilines text in Markdown format with information about one instrument. 663 """ 664 splitLine = "| | |\n" 665 infoText = "" 666 667 if iJSON is not None and iJSON and isinstance(iJSON, dict): 668 info = [ 669 "# Main information\n\n", 670 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 671 "| Parameters | Values |\n", 672 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 673 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 674 "| Full name: | {:<54} |\n".format(iJSON["name"]), 675 ] 676 677 if "sector" in iJSON.keys() and iJSON["sector"]: 678 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 679 680 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 681 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 682 683 info.extend([ 684 splitLine, 685 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 686 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 687 ]) 688 689 if "isin" in iJSON.keys() and iJSON["isin"]: 690 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 691 692 if "classCode" in iJSON.keys(): 693 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 694 695 info.extend([ 696 splitLine, 697 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 698 splitLine, 699 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 700 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 701 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 702 ]) 703 704 if iJSON["figi"]: 705 self._figi = iJSON["figi"] 706 iJSON = iJSON | self.RequestTradingStatus() 707 708 info.extend([ 709 splitLine, 710 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 711 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 712 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 713 ]) 714 715 info.append(splitLine) 716 717 if "type" in iJSON.keys() and iJSON["type"]: 718 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 719 720 if "shareType" in iJSON.keys() and iJSON["shareType"]: 721 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 722 723 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 724 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 725 726 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 727 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 728 729 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 730 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 731 732 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 733 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 734 735 if "focusType" in iJSON.keys() and iJSON["focusType"]: 736 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 737 738 if "assetType" in iJSON.keys() and iJSON["assetType"]: 739 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 740 741 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 742 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 743 744 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 745 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 746 747 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 748 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 749 750 if "currency" in iJSON.keys(): 751 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 752 753 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 754 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 755 756 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 757 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 758 759 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 760 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 761 762 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 763 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 764 765 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 766 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 767 768 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 769 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 770 771 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 772 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 773 774 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 775 info.append("| Perpetual bond: | Yes |\n") 776 777 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 778 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 779 780 iExt = None 781 if iJSON["type"] == "Bonds": 782 info.extend([ 783 splitLine, 784 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 785 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 786 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 787 iJSON["nominal"]["currency"], 788 )), 789 ]) 790 791 if "floatingCouponFlag" in iJSON.keys(): 792 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 793 794 if "amortizationFlag" in iJSON.keys(): 795 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 796 797 info.append(splitLine) 798 799 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 800 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 801 802 if iJSON["figi"]: 803 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 804 805 info.extend([ 806 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 807 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 808 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 809 ]) 810 811 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 812 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 813 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 814 iJSON["aciValue"]["currency"] 815 ))) 816 817 if "currentPrice" in iJSON.keys(): 818 info.append(splitLine) 819 820 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 821 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 822 823 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 824 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 825 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 826 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 827 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 828 829 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 830 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 831 832 info.extend([ 833 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 834 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 835 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 836 )), 837 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 838 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 839 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 840 )), 841 "| Changes between last deal price and last close | {:<54} |\n".format( 842 "{:.2f}%{}".format( 843 iJSON["currentPrice"]["changes"], 844 " ({}{:.2f} {})".format( 845 "+" if bondChangesDelta > 0 else "", 846 bondChangesDelta, 847 aciCurrency 848 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 849 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 850 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 851 currency 852 ), 853 ) 854 ), 855 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 856 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 857 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 858 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 859 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 860 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 861 )), 862 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 863 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 864 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 865 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 866 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 867 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 868 )), 869 ]) 870 871 if "lot" in iJSON.keys(): 872 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 873 874 if "step" in iJSON.keys() and iJSON["step"] != 0: 875 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 876 877 # Add bond payment calendar: 878 if iJSON["type"] == "Bonds": 879 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 880 info.extend(["\n#", strCalendar]) 881 882 infoText += "".join(info) 883 884 if show and not onlyFiles: 885 uLogger.info("{}".format(infoText)) 886 887 if self.infoFile is not None and (show or onlyFiles): 888 with open(self.infoFile, "w", encoding="UTF-8") as fH: 889 fH.write(infoText) 890 891 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 892 893 if self.useHTMLReports: 894 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 895 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 896 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 897 898 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 899 900 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self._ticker] - show: if
Truethen also printing information about instrument and its current price. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format with information about one instrument.
902 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 903 """ 904 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 905 906 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 907 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 908 :return: JSON formatted data with information about instrument. 909 """ 910 tickerJSON = {} 911 if self.moreDebug: 912 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 913 914 if not self._ticker: 915 uLogger.warning("self._ticker variable is not be empty!") 916 917 else: 918 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 919 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 920 raise Exception("Instrument not allowed") 921 922 if not self.iList: 923 self.iList = self.Listing() 924 925 if self._ticker in self.iList["Shares"].keys(): 926 tickerJSON = self.iList["Shares"][self._ticker] 927 if self.moreDebug: 928 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 929 930 elif self._ticker in self.iList["Currencies"].keys(): 931 tickerJSON = self.iList["Currencies"][self._ticker] 932 if self.moreDebug: 933 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 934 935 elif self._ticker in self.iList["Bonds"].keys(): 936 tickerJSON = self.iList["Bonds"][self._ticker] 937 if self.moreDebug: 938 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 939 940 elif self._ticker in self.iList["Etfs"].keys(): 941 tickerJSON = self.iList["Etfs"][self._ticker] 942 if self.moreDebug: 943 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 944 945 elif self._ticker in self.iList["Futures"].keys(): 946 tickerJSON = self.iList["Futures"][self._ticker] 947 if self.moreDebug: 948 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 949 950 if tickerJSON: 951 self._figi = tickerJSON["figi"] 952 953 if requestPrice: 954 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 955 956 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 957 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 958 959 else: 960 tickerJSON["currentPrice"]["changes"] = 0 961 962 if show: 963 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 964 965 else: 966 if show: 967 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 968 969 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
971 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 972 """ 973 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 974 975 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 976 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 977 :return: JSON formatted data with information about instrument. 978 """ 979 figiJSON = {} 980 if self.moreDebug: 981 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 982 983 if not self._figi: 984 uLogger.warning("self._figi variable is not be empty!") 985 986 else: 987 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 988 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 989 raise Exception("Instrument not allowed") 990 991 if not self.iList: 992 self.iList = self.Listing() 993 994 for item in self.iList["Shares"].keys(): 995 if self._figi == self.iList["Shares"][item]["figi"]: 996 figiJSON = self.iList["Shares"][item] 997 998 if self.moreDebug: 999 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1000 1001 break 1002 1003 if not figiJSON: 1004 for item in self.iList["Currencies"].keys(): 1005 if self._figi == self.iList["Currencies"][item]["figi"]: 1006 figiJSON = self.iList["Currencies"][item] 1007 1008 if self.moreDebug: 1009 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1010 1011 break 1012 1013 if not figiJSON: 1014 for item in self.iList["Bonds"].keys(): 1015 if self._figi == self.iList["Bonds"][item]["figi"]: 1016 figiJSON = self.iList["Bonds"][item] 1017 1018 if self.moreDebug: 1019 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1020 1021 break 1022 1023 if not figiJSON: 1024 for item in self.iList["Etfs"].keys(): 1025 if self._figi == self.iList["Etfs"][item]["figi"]: 1026 figiJSON = self.iList["Etfs"][item] 1027 1028 if self.moreDebug: 1029 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1030 1031 break 1032 1033 if not figiJSON: 1034 for item in self.iList["Futures"].keys(): 1035 if self._figi == self.iList["Futures"][item]["figi"]: 1036 figiJSON = self.iList["Futures"][item] 1037 1038 if self.moreDebug: 1039 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1040 1041 break 1042 1043 if figiJSON: 1044 self._figi = figiJSON["figi"] 1045 self._ticker = figiJSON["ticker"] 1046 1047 if requestPrice: 1048 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1049 1050 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1051 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1052 1053 else: 1054 figiJSON["currentPrice"]["changes"] = 0 1055 1056 if show: 1057 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1058 1059 else: 1060 if show: 1061 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1062 1063 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1065 def GetCurrentPrices(self, show: bool = True) -> dict: 1066 """ 1067 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1068 `{"buy": [{"price": 1243.8, "quantity": 193}, 1069 {"price": 1244.0, "quantity": 168}, 1070 {"price": 1244.8, "quantity": 5}, 1071 {"price": 1245.0, "quantity": 61}, 1072 {"price": 1245.4, "quantity": 60}], 1073 "sell": [{"price": 1243.6, "quantity": 8}, 1074 {"price": 1242.6, "quantity": 10}, 1075 {"price": 1242.4, "quantity": 18}, 1076 {"price": 1242.2, "quantity": 50}, 1077 {"price": 1242.0, "quantity": 113}], 1078 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1079 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1080 - sell: list of dicts with Buyers prices, 1081 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1082 - quantity: volume value by current price in lots, 1083 - limitUp: current trade session limit price, maximum, 1084 - limitDown: current trade session limit price, minimum, 1085 - lastPrice: last deal price of the instrument, 1086 - closePrice: previous trade session close price of the instrument. 1087 1088 See also: `SearchByTicker()` and `SearchByFIGI()`. 1089 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1090 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1091 1092 :param show: if `True` then print DOM to log and console. 1093 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1094 If an error occurred then returns an empty record: 1095 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1096 """ 1097 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1098 1099 if self.depth < 1: 1100 uLogger.error("Depth of Market (DOM) must be >=1!") 1101 raise Exception("Incorrect value") 1102 1103 if not (self._ticker or self._figi): 1104 uLogger.error("self._ticker or self._figi variables must be defined!") 1105 raise Exception("Ticker or FIGI required") 1106 1107 if self._ticker and not self._figi: 1108 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1109 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1110 1111 if not self._ticker and self._figi: 1112 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1113 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1114 1115 if not self._figi: 1116 uLogger.error("FIGI is not defined!") 1117 raise Exception("Ticker or FIGI required") 1118 1119 else: 1120 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1121 1122 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1123 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1124 self.body = str({"figi": self._figi, "depth": self.depth}) 1125 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1126 1127 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1128 # list of dicts with sellers orders: 1129 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1130 1131 # list of dicts with buyers orders: 1132 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1133 1134 # max price of instrument at this time: 1135 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1136 1137 # min price of instrument at this time: 1138 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1139 1140 # last price of deal with instrument: 1141 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1142 1143 # last close price of instrument: 1144 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1145 1146 else: 1147 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1148 uLogger.debug("Server response: {}".format(pricesResponse)) 1149 1150 if show: 1151 if prices["buy"] or prices["sell"]: 1152 info = [ 1153 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1154 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1155 self._ticker, 1156 self._figi, 1157 self.depth, 1158 ), 1159 "-" * 60, "\n", 1160 " Orders of Buyers | Orders of Sellers\n", 1161 "-" * 60, "\n", 1162 " Sell prices (volumes) | Buy prices (volumes)\n", 1163 "-" * 60, "\n", 1164 ] 1165 1166 if not prices["buy"]: 1167 info.append(" | No orders!\n") 1168 sumBuy = 0 1169 1170 else: 1171 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1172 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1173 for item in maxMinSorted: 1174 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1175 1176 if not prices["sell"]: 1177 info.append("No orders! |\n") 1178 sumSell = 0 1179 1180 else: 1181 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1182 for item in prices["sell"]: 1183 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1184 1185 info.extend([ 1186 "-" * 60, "\n", 1187 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1188 "-" * 60, "\n", 1189 ]) 1190 1191 infoText = "".join(info) 1192 1193 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1194 1195 else: 1196 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1197 1198 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1200 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1201 """ 1202 This method get and show information about all available broker instruments for current user account. 1203 If `instrumentsFile` string is not empty then also save information to this file. 1204 1205 :param show: if `True` then print results to console, if `False` — print only to file. 1206 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1207 :return: multi-lines string with all available broker instruments. 1208 """ 1209 if not self.iList: 1210 self.iList = self.Listing() 1211 1212 info = [ 1213 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1214 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1215 ] 1216 1217 # add instruments count by type: 1218 for iType in self.iList.keys(): 1219 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1220 1221 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1222 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1223 1224 # generating info tables with all instruments by type: 1225 for iType in self.iList.keys(): 1226 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1227 1228 for instrument in self.iList[iType].keys(): 1229 iName = self.iList[iType][instrument]["name"] # instrument's name 1230 if len(iName) > 57: 1231 iName = "{}...".format(iName[:54]) # right trim for a long string 1232 1233 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1234 self.iList[iType][instrument]["ticker"], 1235 iName, 1236 self.iList[iType][instrument]["figi"], 1237 self.iList[iType][instrument]["currency"], 1238 self.iList[iType][instrument]["lot"], 1239 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1240 )) 1241 1242 infoText = "".join(info) 1243 1244 if show and not onlyFiles: 1245 uLogger.info(infoText) 1246 1247 if self.instrumentsFile and (show or onlyFiles): 1248 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1249 fH.write(infoText) 1250 1251 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1252 1253 if self.useHTMLReports: 1254 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1255 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1256 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1257 1258 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1259 1260 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse— print only to file. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multi-lines string with all available broker instruments.
1262 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1263 """ 1264 This method search and show information about instruments by part of its ticker, FIGI or name. 1265 If `searchResultsFile` string is not empty then also save information to this file. 1266 1267 :param pattern: string with part of ticker, FIGI or instrument's name. 1268 :param show: if `True` then print results to console, if `False` — return list of result only. 1269 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1270 :return: list of dictionaries with all found instruments. 1271 """ 1272 if not self.iList: 1273 self.iList = self.Listing() 1274 1275 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1276 compiledPattern = re.compile(pattern, re.IGNORECASE) 1277 1278 for iType in self.iList: 1279 for instrument in self.iList[iType].values(): 1280 searchResult = compiledPattern.search(" ".join( 1281 [instrument["ticker"], instrument["figi"], instrument["name"]] 1282 )) 1283 1284 if searchResult: 1285 searchResults[iType][instrument["ticker"]] = instrument 1286 1287 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1288 info = [ 1289 "# Search results\n\n", 1290 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1291 "* **Search pattern:** [{}]\n".format(pattern), 1292 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1293 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1294 ] 1295 infoShort = info[:] 1296 1297 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1298 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1299 skippedLine = "| ... | ... | ... | ... |\n" 1300 1301 if resultsLen == 0: 1302 info.append("\nNo results\n") 1303 infoShort.append("\nNo results\n") 1304 uLogger.warning("No results. Try changing your search pattern.") 1305 1306 else: 1307 for iType in searchResults: 1308 iTypeValuesCount = len(searchResults[iType].values()) 1309 if iTypeValuesCount > 0: 1310 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1311 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1312 1313 for instrument in searchResults[iType].values(): 1314 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1315 instrument["type"], 1316 instrument["ticker"], 1317 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1318 instrument["figi"], 1319 )) 1320 1321 if iTypeValuesCount <= 5: 1322 infoShort.extend(info[-iTypeValuesCount:]) 1323 1324 else: 1325 infoShort.extend(info[-5:]) 1326 infoShort.append(skippedLine) 1327 1328 infoText = "".join(info) 1329 infoTextShort = "".join(infoShort) 1330 1331 if show and not onlyFiles: 1332 uLogger.info(infoTextShort) 1333 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1334 1335 if self.searchResultsFile and (show or onlyFiles): 1336 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1337 fH.write(infoText) 1338 1339 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1340 1341 if self.useHTMLReports: 1342 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1343 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1344 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1345 1346 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1347 1348 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse— return list of result only. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
list of dictionaries with all found instruments.
1350 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1351 """ 1352 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1353 1354 :param instruments: list of strings with tickers or FIGIs. 1355 :return: list with unique instrument FIGIs only. 1356 """ 1357 requestedInstruments = [] 1358 for iName in instruments: 1359 if iName not in self.aliases.keys(): 1360 if iName not in requestedInstruments: 1361 requestedInstruments.append(iName) 1362 1363 else: 1364 if iName not in requestedInstruments: 1365 if self.aliases[iName] not in requestedInstruments: 1366 requestedInstruments.append(self.aliases[iName]) 1367 1368 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1369 1370 onlyUniqueFIGIs = [] 1371 for iName in requestedInstruments: 1372 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1373 continue 1374 1375 self._ticker = iName 1376 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1377 1378 if not iData: 1379 self._ticker = "" 1380 self._figi = iName 1381 1382 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1383 1384 if not iData: 1385 self._figi = "" 1386 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1387 1388 if iData and iData["figi"] not in onlyUniqueFIGIs: 1389 onlyUniqueFIGIs.append(iData["figi"]) 1390 1391 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1392 1393 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1395 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1396 """ 1397 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1398 1399 See limits: https://tinkoff.github.io/investAPI/limits/ 1400 1401 If `pricesFile` string is not empty then also save information to this file. 1402 1403 :param instruments: list of strings with tickers or FIGIs. 1404 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1405 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1406 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1407 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1408 """ 1409 if instruments is None or not instruments: 1410 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1411 raise Exception("Ticker or FIGI required") 1412 1413 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1414 1415 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1416 1417 iList = [] # trying to get info and current prices about all unique instruments: 1418 for self._figi in onlyUniqueFIGIs: 1419 iData = self.SearchByFIGI(requestPrice=True, show=False) 1420 iList.append(iData) 1421 1422 self.ShowListOfPrices(iList, show, onlyFiles) 1423 1424 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1426 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1427 """ 1428 Show table contains current prices of given instruments. 1429 1430 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1431 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1432 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1433 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1434 :return: multilines text in Markdown format as a table contains current prices. 1435 """ 1436 infoText = "" 1437 1438 if show or self.pricesFile or onlyFiles: 1439 info = [ 1440 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1441 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1442 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1443 ] 1444 1445 for item in iList: 1446 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1447 item["ticker"], 1448 item["figi"], 1449 item["type"], 1450 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1451 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1452 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1453 "{} / {}".format( 1454 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1455 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1456 ), 1457 "{} / {}".format( 1458 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1459 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1460 ), 1461 item["currency"], 1462 )) 1463 1464 infoText = "".join(info) 1465 1466 if show and not onlyFiles: 1467 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1468 1469 if self.pricesFile and (show or onlyFiles): 1470 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1471 fH.write(infoText) 1472 1473 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1474 1475 if self.useHTMLReports: 1476 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1477 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1478 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1479 1480 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1481 1482 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format as a table contains current prices.
1484 def RequestTradingStatus(self) -> dict: 1485 """ 1486 Requesting trading status for the instrument defined by `figi` variable. 1487 1488 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1489 1490 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1491 1492 :return: dictionary with trading status attributes. Response example: 1493 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1494 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1495 """ 1496 if self._figi is None or not self._figi: 1497 uLogger.error("Variable `figi` must be defined for using this method!") 1498 raise Exception("FIGI required") 1499 1500 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1501 1502 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1503 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1504 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1505 1506 if self.moreDebug: 1507 uLogger.debug("Records about current trading status successfully received") 1508 1509 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1511 def RequestPortfolio(self) -> dict: 1512 """ 1513 Requesting actual user's portfolio for current `accountId`. 1514 1515 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1516 1517 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1518 1519 :return: dictionary with user's portfolio. 1520 """ 1521 if self.accountId is None or not self.accountId: 1522 uLogger.error("Variable `accountId` must be defined for using this method!") 1523 raise Exception("Account ID required") 1524 1525 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1526 1527 self.body = str({"accountId": self.accountId}) 1528 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1529 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1530 1531 if self.moreDebug: 1532 uLogger.debug("Records about user's portfolio successfully received") 1533 1534 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1536 def RequestPositions(self) -> dict: 1537 """ 1538 Requesting open positions by currencies and instruments for current `accountId`. 1539 1540 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1541 1542 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1543 1544 :return: dictionary with open positions by instruments. 1545 """ 1546 if self.accountId is None or not self.accountId: 1547 uLogger.error("Variable `accountId` must be defined for using this method!") 1548 raise Exception("Account ID required") 1549 1550 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1551 1552 self.body = str({"accountId": self.accountId}) 1553 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1554 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1555 1556 if self.moreDebug: 1557 uLogger.debug("Records about current open positions successfully received") 1558 1559 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1561 def RequestPendingOrders(self) -> list: 1562 """ 1563 Requesting current actual pending limit orders for current `accountId`. 1564 1565 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1566 1567 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1568 1569 :return: list of dictionaries with pending limit orders. 1570 """ 1571 if self.accountId is None or not self.accountId: 1572 uLogger.error("Variable `accountId` must be defined for using this method!") 1573 raise Exception("Account ID required") 1574 1575 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1576 1577 self.body = str({"accountId": self.accountId}) 1578 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1579 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1580 1581 if "orders" in rawResponse.keys(): 1582 rawOrders = rawResponse["orders"] 1583 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1584 1585 else: 1586 rawOrders = [] 1587 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1588 1589 return rawOrders
Requesting current actual pending limit orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending limit orders.
1591 def RequestStopOrders(self) -> list: 1592 """ 1593 Requesting current actual stop orders for current `accountId`. 1594 1595 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1596 1597 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1598 1599 :return: list of dictionaries with stop orders. 1600 """ 1601 if self.accountId is None or not self.accountId: 1602 uLogger.error("Variable `accountId` must be defined for using this method!") 1603 raise Exception("Account ID required") 1604 1605 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1606 1607 self.body = str({"accountId": self.accountId}) 1608 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1609 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1610 1611 if "stopOrders" in rawResponse.keys(): 1612 rawStopOrders = rawResponse["stopOrders"] 1613 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1614 1615 else: 1616 rawStopOrders = [] 1617 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1618 1619 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1621 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1622 """ 1623 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1624 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1625 and `overviewBondsCalendarFile` are defined then also save information to file. 1626 1627 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1628 many requests about the state of the portfolio, and then, based on the received data, a large number 1629 of calculation and statistics are collected. 1630 1631 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1632 :param details: how detailed should the information be? 1633 - `full` — shows full available information about portfolio status (by default), 1634 - `positions` — shows only open positions, 1635 - `orders` — shows only sections of open limits and stop orders. 1636 - `digest` — show a short digest of the portfolio status, 1637 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1638 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1639 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1640 :return: dictionary with client's raw portfolio and some statistics. 1641 """ 1642 if self.accountId is None or not self.accountId: 1643 uLogger.error("Variable `accountId` must be defined for using this method!") 1644 raise Exception("Account ID required") 1645 1646 view = { 1647 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1648 "headers": {}, # list of dictionaries, response headers without "positions" section 1649 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1650 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1651 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1652 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1653 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1654 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1655 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1656 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1657 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1658 }, 1659 "stat": { # --- some statistics calculated using "raw" sections: 1660 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1661 "availableRUB": 0., # available rubles (without other currencies) 1662 "blockedRUB": 0., # blocked sum in Russian Rouble 1663 "totalChangesRUB": 0., # changes for all open trades in RUB 1664 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1665 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1666 "sharesCostRUB": 0., # costs of all shares in RUB 1667 "bondsCostRUB": 0., # costs of all bonds in RUB 1668 "etfsCostRUB": 0., # costs of all etfs in RUB 1669 "futuresCostRUB": 0., # costs of all futures in RUB 1670 "Currencies": [], # list of dictionaries of all currencies statistics 1671 "Shares": [], # list of dictionaries of all shares statistics 1672 "Bonds": [], # list of dictionaries of all bonds statistics 1673 "Etfs": [], # list of dictionaries of all etfs statistics 1674 "Futures": [], # list of dictionaries of all futures statistics 1675 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1676 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1677 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1678 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1679 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1680 }, 1681 "analytics": { # --- some analytics of portfolio: 1682 "distrByAssets": {}, # portfolio distribution by assets 1683 "distrByCompanies": {}, # portfolio distribution by companies 1684 "distrBySectors": {}, # portfolio distribution by sectors 1685 "distrByCurrencies": {}, # portfolio distribution by currencies 1686 "distrByCountries": {}, # portfolio distribution by countries 1687 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1688 } 1689 } 1690 1691 details = details.lower() 1692 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1693 if details not in availableDetails: 1694 details = "full" 1695 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1696 1697 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1698 1699 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1700 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1701 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1702 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1703 1704 # save response headers without "positions" section: 1705 for key in portfolioResponse.keys(): 1706 if key != "positions": 1707 view["raw"]["headers"][key] = portfolioResponse[key] 1708 1709 else: 1710 continue 1711 1712 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1713 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1714 for item in portfolioResponse["positions"]: 1715 if item["instrumentType"] == "currency": 1716 self._figi = item["figi"] 1717 if not self._figi and item["ticker"]: 1718 self._ticker = item["ticker"] 1719 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1720 1721 curr = self.SearchByFIGI(requestPrice=False) 1722 1723 # current price of currency in RUB: 1724 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1725 "name": curr["name"], 1726 "currentPrice": NanoToFloat( 1727 item["currentPrice"]["units"], 1728 item["currentPrice"]["nano"] 1729 ), 1730 } 1731 1732 view["raw"]["Currencies"].append(item) 1733 1734 elif item["instrumentType"] == "share": 1735 view["raw"]["Shares"].append(item) 1736 1737 elif item["instrumentType"] == "bond": 1738 view["raw"]["Bonds"].append(item) 1739 1740 elif item["instrumentType"] == "etf": 1741 view["raw"]["Etfs"].append(item) 1742 1743 elif item["instrumentType"] == "futures": 1744 view["raw"]["Futures"].append(item) 1745 1746 else: 1747 continue 1748 1749 # how many volume of currencies (by ISO currency name) are blocked: 1750 for item in view["raw"]["positions"]["blocked"]: 1751 blocked = NanoToFloat(item["units"], item["nano"]) 1752 if blocked > 0: 1753 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1754 1755 # how many volume of instruments (by FIGI) are blocked: 1756 for item in view["raw"]["positions"]["securities"]: 1757 blocked = int(item["blocked"]) 1758 if blocked > 0: 1759 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1760 1761 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1762 1763 if "rub" in allBlocked.keys(): 1764 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1765 1766 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1767 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1768 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1769 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1770 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1771 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1772 view["stat"]["portfolioCostRUB"] = sum([ 1773 view["stat"]["allCurrenciesCostRUB"], 1774 view["stat"]["sharesCostRUB"], 1775 view["stat"]["bondsCostRUB"], 1776 view["stat"]["etfsCostRUB"], 1777 view["stat"]["futuresCostRUB"], 1778 ]) 1779 1780 # --- calculating some portfolio statistics: 1781 byComp = {} # distribution by companies 1782 bySect = {} # distribution by sectors 1783 byCurr = {} # distribution by currencies (include RUB) 1784 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1785 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1786 1787 for item in portfolioResponse["positions"]: 1788 self._figi = item["figi"] 1789 if not self._figi and item["ticker"]: 1790 self._ticker = item["ticker"] 1791 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1792 1793 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1794 1795 if instrument: 1796 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1797 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1798 1799 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1800 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1801 1802 else: 1803 blocked = 0 1804 1805 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1806 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1807 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1808 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1809 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1810 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1811 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1812 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1813 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1814 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1815 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1816 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1817 1818 statData = { 1819 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1820 "ticker": instrument["ticker"], # ticker by FIGI 1821 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1822 "volume": volume, # available volume of instrument 1823 "lots": lots, # volume in lots of instrument 1824 "direction": direction, # direction of an instrument's position: short or long 1825 "blocked": blocked, # blocked volume of currency or instrument 1826 "currentPrice": curPrice, # current instrument's price in basic asset 1827 "average": average, # current average position price 1828 "cost": cost, # current cost of all volume of instrument in basic asset 1829 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1830 "costRUB": costRUB, # cost of instrument in ruble 1831 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1832 "profit": profit, # expected profit at current moment 1833 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1834 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1835 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1836 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1837 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1838 "step": instrument["step"], # minimum price increment 1839 } 1840 1841 # adding distribution by unique countries: 1842 if statData["country"] not in byCountry.keys(): 1843 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1844 1845 else: 1846 byCountry[statData["country"]]["cost"] += costRUB 1847 byCountry[statData["country"]]["percent"] += percentCostRUB 1848 1849 if item["instrumentType"] != "currency": 1850 # adding distribution by unique companies: 1851 if statData["name"]: 1852 if statData["name"] not in byComp.keys(): 1853 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1854 1855 else: 1856 byComp[statData["name"]]["cost"] += costRUB 1857 byComp[statData["name"]]["percent"] += percentCostRUB 1858 1859 # adding distribution by unique sectors: 1860 if statData["sector"] not in bySect.keys(): 1861 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1862 1863 else: 1864 bySect[statData["sector"]]["cost"] += costRUB 1865 bySect[statData["sector"]]["percent"] += percentCostRUB 1866 1867 # adding distribution by unique currencies: 1868 if currency not in byCurr.keys(): 1869 byCurr[currency] = { 1870 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1871 "cost": costRUB, 1872 "percent": percentCostRUB 1873 } 1874 1875 else: 1876 byCurr[currency]["cost"] += costRUB 1877 byCurr[currency]["percent"] += percentCostRUB 1878 1879 # saving statistics for every instrument: 1880 if item["instrumentType"] == "currency": 1881 view["stat"]["Currencies"].append(statData) 1882 1883 # update dict with free funds for trading (total - blocked) by currencies 1884 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1885 view["stat"]["funds"][currency] = { 1886 "total": volume, 1887 "totalCostRUB": costRUB, # total volume cost in rubles 1888 "free": volume - blocked, 1889 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1890 } 1891 1892 elif item["instrumentType"] == "share": 1893 view["stat"]["Shares"].append(statData) 1894 1895 elif item["instrumentType"] == "bond": 1896 view["stat"]["Bonds"].append(statData) 1897 1898 elif item["instrumentType"] == "etf": 1899 view["stat"]["Etfs"].append(statData) 1900 1901 elif item["instrumentType"] == "Futures": 1902 view["stat"]["Futures"].append(statData) 1903 1904 else: 1905 continue 1906 1907 # total changes in Russian Ruble: 1908 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1909 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1910 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1911 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1912 view["stat"]["funds"]["rub"] = { 1913 "total": view["stat"]["availableRUB"], 1914 "totalCostRUB": view["stat"]["availableRUB"], 1915 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1916 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1917 } 1918 1919 # --- pending limit orders sector data: 1920 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1921 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1922 1923 for item in view["raw"]["orders"]: 1924 self._figi = item["figi"] 1925 1926 if item["figi"] not in uniquePendingOrdersFIGIs: 1927 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1928 1929 uniquePendingOrdersFIGIs.append(item["figi"]) 1930 uniquePendingOrders[item["figi"]] = instrument 1931 1932 else: 1933 instrument = uniquePendingOrders[item["figi"]] 1934 1935 if instrument: 1936 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1937 orderType = TKS_ORDER_TYPES[item["orderType"]] 1938 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1939 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1940 1941 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1942 if item["direction"] == "ORDER_DIRECTION_BUY": 1943 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1944 1945 else: 1946 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1947 1948 # requested price for order execution: 1949 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1950 1951 # necessary changes in percent to reach target from current price: 1952 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1953 1954 view["stat"]["orders"].append({ 1955 "orderID": item["orderId"], # orderId number parameter of current order 1956 "figi": item["figi"], # FIGI identification 1957 "ticker": instrument["ticker"], # ticker name by FIGI 1958 "lotsRequested": item["lotsRequested"], # requested lots value 1959 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1960 "currentPrice": lastPrice, # current instrument's price for defined action 1961 "targetPrice": target, # requested price for order execution in base currency 1962 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1963 "percentChanges": changes, # changes in percent to target from current price 1964 "currency": item["currency"], # instrument's currency name 1965 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1966 "type": orderType, # type of order from TKS_ORDER_TYPES 1967 "status": orderState, # order status from TKS_ORDER_STATES 1968 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1969 }) 1970 1971 # --- stop orders sector data: 1972 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1973 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1974 1975 for item in view["raw"]["stopOrders"]: 1976 self._figi = item["figi"] 1977 1978 if item["figi"] not in uniqueStopOrdersFIGIs: 1979 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1980 1981 uniqueStopOrdersFIGIs.append(item["figi"]) 1982 uniqueStopOrders[item["figi"]] = instrument 1983 1984 else: 1985 instrument = uniqueStopOrders[item["figi"]] 1986 1987 if instrument: 1988 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1989 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1990 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1991 1992 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1993 if "expirationTime" in item.keys(): 1994 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1995 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1996 1997 else: 1998 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1999 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 2000 2001 # current instrument's price (last sellers order if buy, and last buyers order if sell): 2002 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 2003 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2004 2005 else: 2006 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2007 2008 # requested price when stop-order executed: 2009 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2010 2011 # price for limit-order, set up when stop-order executed: 2012 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2013 2014 # necessary changes in percent to reach target from current price: 2015 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2016 2017 view["stat"]["stopOrders"].append({ 2018 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2019 "figi": item["figi"], # FIGI identification 2020 "ticker": instrument["ticker"], # ticker name by FIGI 2021 "lotsRequested": item["lotsRequested"], # requested lots value 2022 "currentPrice": lastPrice, # current instrument's price for defined action 2023 "targetPrice": target, # requested price for stop-order execution in base currency 2024 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2025 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2026 "percentChanges": changes, # changes in percent to target from current price 2027 "currency": item["currency"], # instrument's currency name 2028 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2029 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2030 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2031 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2032 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2033 }) 2034 2035 # --- calculating data for analytics section: 2036 # portfolio distribution by assets: 2037 view["analytics"]["distrByAssets"] = { 2038 "Ruble": { 2039 "uniques": 1, 2040 "cost": view["stat"]["availableRUB"], 2041 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2042 }, 2043 "Currencies": { 2044 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2045 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2046 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2047 }, 2048 "Shares": { 2049 "uniques": len(view["stat"]["Shares"]), 2050 "cost": view["stat"]["sharesCostRUB"], 2051 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2052 }, 2053 "Bonds": { 2054 "uniques": len(view["stat"]["Bonds"]), 2055 "cost": view["stat"]["bondsCostRUB"], 2056 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2057 }, 2058 "Etfs": { 2059 "uniques": len(view["stat"]["Etfs"]), 2060 "cost": view["stat"]["etfsCostRUB"], 2061 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2062 }, 2063 "Futures": { 2064 "uniques": len(view["stat"]["Futures"]), 2065 "cost": view["stat"]["futuresCostRUB"], 2066 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2067 }, 2068 } 2069 2070 # portfolio distribution by companies: 2071 view["analytics"]["distrByCompanies"]["All money cash"] = { 2072 "ticker": "", 2073 "cost": view["stat"]["allCurrenciesCostRUB"], 2074 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2075 } 2076 view["analytics"]["distrByCompanies"].update(byComp) 2077 2078 # portfolio distribution by sectors: 2079 view["analytics"]["distrBySectors"]["All money cash"] = { 2080 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2081 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2082 } 2083 view["analytics"]["distrBySectors"].update(bySect) 2084 2085 # portfolio distribution by currencies: 2086 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2087 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2088 2089 if self.moreDebug: 2090 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2091 2092 view["analytics"]["distrByCurrencies"].update(byCurr) 2093 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2094 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2095 2096 # portfolio distribution by countries: 2097 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2098 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2099 2100 if self.moreDebug: 2101 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2102 2103 view["analytics"]["distrByCountries"].update(byCountry) 2104 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2105 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2106 2107 # --- Prepare text statistics overview in human-readable: 2108 if show or onlyFiles: 2109 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2110 2111 # Whatever the value `details`, header not changes: 2112 info = [ 2113 "# Client's portfolio\n\n", 2114 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2115 "* **Account ID:** [{}]\n".format(self.accountId), 2116 ] 2117 2118 if details in ["full", "positions", "digest"]: 2119 info.extend([ 2120 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2121 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2122 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2123 view["stat"]["totalChangesRUB"], 2124 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2125 view["stat"]["totalChangesPercentRUB"], 2126 ), 2127 ]) 2128 2129 if details in ["full", "positions"]: 2130 info.extend([ 2131 "## Open positions\n\n", 2132 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2133 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2134 "| **Ruble:** | {:>31} | | | | | |\n".format( 2135 "{:.2f} ({:.2f}) rub".format( 2136 view["stat"]["availableRUB"], 2137 view["stat"]["blockedRUB"], 2138 ) 2139 ) 2140 ]) 2141 2142 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2143 return [ 2144 "| | | | | | | |\n", 2145 "| {:<27} | | | | | {:>19} | |\n".format( 2146 noTradeStr if noTradeStr else typeStr, 2147 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2148 ), 2149 ] 2150 2151 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2152 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2153 "{} [{}]".format(data["ticker"], data["figi"]), 2154 "{:.2f} ({:.2f}) {}".format( 2155 data["volume"], 2156 data["blocked"], 2157 data["currency"], 2158 ) if isCurr else "{:.0f} ({:.0f})".format( 2159 data["volume"], 2160 data["blocked"], 2161 ), 2162 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2163 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2164 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2165 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2166 "{}{:.2f} {} ({}{:.2f}%)".format( 2167 "+" if data["profit"] > 0 else "", 2168 data["profit"], data["baseCurrencyName"], 2169 "+" if data["percentProfit"] > 0 else "", 2170 data["percentProfit"], 2171 ), 2172 ) 2173 2174 # --- Show currencies section: 2175 if view["stat"]["Currencies"]: 2176 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2177 for item in view["stat"]["Currencies"]: 2178 info.append(_InfoStr(item, isCurr=True)) 2179 2180 else: 2181 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2182 2183 # --- Show shares section: 2184 if view["stat"]["Shares"]: 2185 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2186 2187 for item in view["stat"]["Shares"]: 2188 info.append(_InfoStr(item)) 2189 2190 else: 2191 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2192 2193 # --- Show bonds section: 2194 if view["stat"]["Bonds"]: 2195 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2196 2197 for item in view["stat"]["Bonds"]: 2198 info.append(_InfoStr(item)) 2199 2200 else: 2201 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2202 2203 # --- Show etfs section: 2204 if view["stat"]["Etfs"]: 2205 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2206 2207 for item in view["stat"]["Etfs"]: 2208 info.append(_InfoStr(item)) 2209 2210 else: 2211 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2212 2213 # --- Show futures section: 2214 if view["stat"]["Futures"]: 2215 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2216 2217 for item in view["stat"]["Futures"]: 2218 info.append(_InfoStr(item)) 2219 2220 else: 2221 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2222 2223 if details in ["full", "orders"]: 2224 # --- Show pending limit orders section: 2225 if view["stat"]["orders"]: 2226 info.extend([ 2227 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2228 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2229 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2230 ]) 2231 2232 for item in view["stat"]["orders"]: 2233 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2234 "{} [{}]".format(item["ticker"], item["figi"]), 2235 item["orderID"], 2236 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2237 "{} {} ({}{:.2f}%)".format( 2238 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2239 item["baseCurrencyName"], 2240 "+" if item["percentChanges"] > 0 else "", 2241 float(item["percentChanges"]), 2242 ), 2243 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2244 item["action"], 2245 item["type"], 2246 item["date"], 2247 )) 2248 2249 else: 2250 info.append("\n## Total pending limit-orders: [0]\n") 2251 2252 # --- Show stop orders section: 2253 if view["stat"]["stopOrders"]: 2254 info.extend([ 2255 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2256 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2257 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2258 ]) 2259 2260 for item in view["stat"]["stopOrders"]: 2261 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2262 "{} [{}]".format(item["ticker"], item["figi"]), 2263 item["orderID"], 2264 item["lotsRequested"], 2265 "{} {} ({}{:.2f}%)".format( 2266 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2267 item["baseCurrencyName"], 2268 "+" if item["percentChanges"] > 0 else "", 2269 float(item["percentChanges"]), 2270 ), 2271 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2272 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2273 item["action"], 2274 item["type"], 2275 item["expType"], 2276 item["createDate"], 2277 item["expDate"], 2278 )) 2279 2280 else: 2281 info.append("\n## Total stop-orders: [0]\n") 2282 2283 if details in ["full", "analytics"]: 2284 # -- Show analytics section: 2285 if view["stat"]["portfolioCostRUB"] > 0: 2286 info.extend([ 2287 "\n# Analytics\n\n" 2288 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2289 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2290 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2291 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2292 view["stat"]["totalChangesRUB"], 2293 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2294 view["stat"]["totalChangesPercentRUB"], 2295 ), 2296 "\n## Portfolio distribution by assets\n" 2297 "\n| Type | Uniques | Percent | Current cost |\n", 2298 "|------------------------------------|---------|---------|--------------------|\n", 2299 ]) 2300 2301 for key in view["analytics"]["distrByAssets"].keys(): 2302 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2303 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2304 key, 2305 view["analytics"]["distrByAssets"][key]["uniques"], 2306 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2307 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2308 )) 2309 2310 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2311 2312 info.extend([ 2313 "\n## Portfolio distribution by companies\n" 2314 "\n| Company | Percent | Current cost |\n", 2315 aSepLine, 2316 ]) 2317 2318 for company in view["analytics"]["distrByCompanies"].keys(): 2319 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2320 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2321 "{}{}".format( 2322 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2323 company, 2324 ), 2325 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2326 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2327 )) 2328 2329 info.extend([ 2330 "\n## Portfolio distribution by sectors\n" 2331 "\n| Sector | Percent | Current cost |\n", 2332 aSepLine, 2333 ]) 2334 2335 for sector in view["analytics"]["distrBySectors"].keys(): 2336 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2337 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2338 sector, 2339 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2340 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2341 )) 2342 2343 info.extend([ 2344 "\n## Portfolio distribution by currencies\n" 2345 "\n| Instruments currencies | Percent | Current cost |\n", 2346 aSepLine, 2347 ]) 2348 2349 for curr in view["analytics"]["distrByCurrencies"].keys(): 2350 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2351 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2352 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2353 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2354 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2355 )) 2356 2357 info.extend([ 2358 "\n## Portfolio distribution by countries\n" 2359 "\n| Assets by country | Percent | Current cost |\n", 2360 aSepLine, 2361 ]) 2362 2363 for country in view["analytics"]["distrByCountries"].keys(): 2364 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2365 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2366 country, 2367 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2368 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2369 )) 2370 2371 if details in ["full", "calendar"]: 2372 # -- Show bonds payment calendar section: 2373 if view["stat"]["Bonds"]: 2374 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2375 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2376 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2377 2378 else: 2379 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2380 2381 infoText = "".join(info) 2382 2383 if show and not onlyFiles: 2384 uLogger.info(infoText) 2385 2386 if details == "full" and self.overviewFile: 2387 filename = self.overviewFile 2388 2389 elif details == "digest" and self.overviewDigestFile: 2390 filename = self.overviewDigestFile 2391 2392 elif details == "positions" and self.overviewPositionsFile: 2393 filename = self.overviewPositionsFile 2394 2395 elif details == "orders" and self.overviewOrdersFile: 2396 filename = self.overviewOrdersFile 2397 2398 elif details == "analytics" and self.overviewAnalyticsFile: 2399 filename = self.overviewAnalyticsFile 2400 2401 elif details == "calendar" and self.overviewBondsCalendarFile: 2402 filename = self.overviewBondsCalendarFile 2403 2404 else: 2405 filename = "" 2406 2407 if filename and (show or onlyFiles): 2408 with open(filename, "w", encoding="UTF-8") as fH: 2409 fH.write(infoText) 2410 2411 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2412 2413 if self.useHTMLReports: 2414 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2415 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2416 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2417 2418 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2419 2420 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
and overviewBondsCalendarFile are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be?
full— shows full available information about portfolio status (by default),positions— shows only open positions,orders— shows only sections of open limits and stop orders.digest— show a short digest of the portfolio status,analytics— shows only the analytics section and the distribution of the portfolio by various categories,calendar— shows only the bonds calendar section (if these present in portfolio).
- onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dictionary with client's raw portfolio and some statistics.
2422 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2423 """ 2424 Returns history operations between two given dates for current `accountId`. 2425 If `reportFile` string is not empty then also save human-readable report. 2426 Shows some statistical data of closed positions. 2427 2428 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2429 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2430 :param show: if `True` then also prints all records to the console. 2431 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2432 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2433 :return: original list of dictionaries with history of deals records from API ("operations" key): 2434 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2435 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2436 """ 2437 if self.accountId is None or not self.accountId: 2438 uLogger.error("Variable `accountId` must be defined for using this method!") 2439 raise Exception("Account ID required") 2440 2441 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2442 2443 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2444 2445 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2446 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2447 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2448 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2449 customStat = {} # custom statistics in additional to responseJSON 2450 2451 # --- output report in human-readable format: 2452 if self.reportFile and (show or onlyFiles): 2453 splitLine1 = "| | | | | |\n" # Summary section 2454 splitLine2 = "| | | | | | | | |\n" # Operations section 2455 nextDay = "" 2456 2457 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2458 2459 if len(ops) > 0: 2460 customStat = { 2461 "opsCount": 0, # total operations count 2462 "buyCount": 0, # buy operations 2463 "sellCount": 0, # sell operations 2464 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2465 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2466 "payIn": {"rub": 0.}, # Deposit brokerage account 2467 "payOut": {"rub": 0.}, # Withdrawals 2468 "divs": {"rub": 0.}, # Dividends income 2469 "coupons": {"rub": 0.}, # Coupon's income 2470 "brokerCom": {"rub": 0.}, # Service commissions 2471 "serviceCom": {"rub": 0.}, # Service commissions 2472 "marginCom": {"rub": 0.}, # Margin commissions 2473 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2474 } 2475 2476 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2477 for item in ops: 2478 if item["state"] == "OPERATION_STATE_EXECUTED": 2479 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2480 2481 # count buy operations: 2482 if "_BUY" in item["operationType"]: 2483 customStat["buyCount"] += 1 2484 2485 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2486 customStat["buyTotal"][item["payment"]["currency"]] += payment 2487 2488 else: 2489 customStat["buyTotal"][item["payment"]["currency"]] = payment 2490 2491 # count sell operations: 2492 elif "_SELL" in item["operationType"]: 2493 customStat["sellCount"] += 1 2494 2495 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2496 customStat["sellTotal"][item["payment"]["currency"]] += payment 2497 2498 else: 2499 customStat["sellTotal"][item["payment"]["currency"]] = payment 2500 2501 # count incoming operations: 2502 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2503 if item["payment"]["currency"] in customStat["payIn"].keys(): 2504 customStat["payIn"][item["payment"]["currency"]] += payment 2505 2506 else: 2507 customStat["payIn"][item["payment"]["currency"]] = payment 2508 2509 # count withdrawals operations: 2510 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2511 if item["payment"]["currency"] in customStat["payOut"].keys(): 2512 customStat["payOut"][item["payment"]["currency"]] += payment 2513 2514 else: 2515 customStat["payOut"][item["payment"]["currency"]] = payment 2516 2517 # count dividends income: 2518 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2519 if item["payment"]["currency"] in customStat["divs"].keys(): 2520 customStat["divs"][item["payment"]["currency"]] += payment 2521 2522 else: 2523 customStat["divs"][item["payment"]["currency"]] = payment 2524 2525 # count coupon's income: 2526 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2527 if item["payment"]["currency"] in customStat["coupons"].keys(): 2528 customStat["coupons"][item["payment"]["currency"]] += payment 2529 2530 else: 2531 customStat["coupons"][item["payment"]["currency"]] = payment 2532 2533 # count broker commissions: 2534 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2535 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2536 customStat["brokerCom"][item["payment"]["currency"]] += payment 2537 2538 else: 2539 customStat["brokerCom"][item["payment"]["currency"]] = payment 2540 2541 # count service commissions: 2542 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2543 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2544 customStat["serviceCom"][item["payment"]["currency"]] += payment 2545 2546 else: 2547 customStat["serviceCom"][item["payment"]["currency"]] = payment 2548 2549 # count margin commissions: 2550 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2551 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2552 customStat["marginCom"][item["payment"]["currency"]] += payment 2553 2554 else: 2555 customStat["marginCom"][item["payment"]["currency"]] = payment 2556 2557 # count withholding taxes: 2558 elif "_TAX" in item["operationType"]: 2559 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2560 customStat["allTaxes"][item["payment"]["currency"]] += payment 2561 2562 else: 2563 customStat["allTaxes"][item["payment"]["currency"]] = payment 2564 2565 else: 2566 continue 2567 2568 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2569 2570 # --- view "Actions" lines: 2571 info.extend([ 2572 "| Report sections | | | | |\n", 2573 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2574 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2575 "| | Buy: {:<22} | {:<28} | | |\n".format( 2576 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2577 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2578 ), 2579 "| | Sell: {:<21} | {:<28} | | |\n".format( 2580 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2581 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2582 ), 2583 ]) 2584 2585 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2586 for key in opsKeys: 2587 if key == "rub": 2588 continue 2589 2590 info.extend([ 2591 "| | | {:<28} | | |\n".format( 2592 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2593 ), 2594 "| | | {:<28} | | |\n".format( 2595 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2596 ), 2597 ]) 2598 2599 info.append(splitLine1) 2600 2601 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2602 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2603 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2604 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2605 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2606 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2607 ) 2608 2609 # --- view "Payments" lines: 2610 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2611 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2612 2613 for key in paymentsKeys: 2614 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2615 2616 info.append(splitLine1) 2617 2618 # --- view "Commissions and taxes" lines: 2619 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2620 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2621 2622 for key in comKeys: 2623 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2624 2625 info.extend([ 2626 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2627 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2628 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2629 ]) 2630 2631 else: 2632 info.append("Broker returned no operations during this period\n") 2633 2634 # --- view "Operations" section: 2635 for item in ops: 2636 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2637 continue 2638 2639 else: 2640 self._figi = item["figi"] 2641 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2642 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2643 2644 # group of deals during one day: 2645 if nextDay and item["date"].split("T")[0] != nextDay: 2646 info.append(splitLine2) 2647 nextDay = "" 2648 2649 else: 2650 nextDay = item["date"].split("T")[0] # saving current day for splitting 2651 2652 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2653 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2654 self._figi if self._figi else "—", 2655 instrument["ticker"] if instrument else "—", 2656 instrument["type"] if instrument else "—", 2657 item["quantity"] if int(item["quantity"]) > 0 else "—", 2658 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2659 TKS_OPERATION_STATES[item["state"]], 2660 TKS_OPERATION_TYPES[item["operationType"]], 2661 )) 2662 2663 infoText = "".join(info) 2664 2665 if show and not onlyFiles: 2666 if self.moreDebug: 2667 uLogger.debug("Records about history of a client's operations successfully received") 2668 2669 uLogger.info(infoText) 2670 2671 if self.reportFile and (show or onlyFiles): 2672 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2673 fH.write(infoText) 2674 2675 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2676 2677 if self.useHTMLReports: 2678 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2679 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2680 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2681 2682 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2683 2684 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2686 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2687 """ 2688 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2689 2690 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2691 Warning! Broker server used ISO UTC time by default. 2692 2693 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2694 Also, `historyFile` used to update history with `onlyMissing` parameter. 2695 2696 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2697 2698 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2699 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2700 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2701 `"hour"`, `"day"`. Default: `"hour"`. 2702 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2703 False by default. Warning! History appends only from last candle to current time 2704 with always update last candle! 2705 :param csvSep: separator if csv-file is used, `,` by default. 2706 :param show: if `True` then also prints Pandas DataFrame to the console. 2707 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2708 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2709 `["date", "time", "open", "high", "low", "close", "volume"]`. 2710 """ 2711 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2712 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2713 history = None # empty pandas object for history 2714 2715 if interval not in TKS_CANDLE_INTERVALS.keys(): 2716 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2717 raise Exception("Incorrect value") 2718 2719 if not (self._ticker or self._figi): 2720 uLogger.error("Ticker or FIGI must be defined!") 2721 raise Exception("Ticker or FIGI required") 2722 2723 if self._ticker and not self._figi: 2724 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2725 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2726 2727 if self._figi and not self._ticker: 2728 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2729 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2730 2731 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2732 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2733 if interval.lower() != "day": 2734 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2735 2736 delta = dtEnd - dtStart # current UTC time minus last time in file 2737 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2738 2739 # calculate history length in candles: 2740 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2741 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2742 length += 1 # to avoid fraction time 2743 2744 # calculate data blocks count: 2745 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2746 2747 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2748 if self.moreDebug: 2749 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2750 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2751 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2752 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2753 2754 tempOld = None # pandas object for old history, if --only-missing key present 2755 lastTime = None # datetime object of last old candle in file 2756 2757 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2758 if self.moreDebug: 2759 uLogger.debug("--only-missing key present, add only last missing candles...") 2760 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2761 2762 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2763 2764 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2765 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2766 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2767 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2768 2769 # get last datetime object from last string in file or minus 1 delta if file is empty: 2770 if len(tempOld) > 0: 2771 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2772 2773 else: 2774 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2775 2776 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2777 2778 responseJSONs = [] # raw history blocks of data 2779 2780 blockEnd = dtEnd 2781 for item in range(blocks): 2782 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2783 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2784 2785 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2786 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2787 )) 2788 2789 if blockStart == blockEnd: 2790 uLogger.debug("Skipped this zero-length block...") 2791 2792 else: 2793 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2794 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2795 self.body = str({ 2796 "figi": self._figi, 2797 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2798 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2799 "interval": TKS_CANDLE_INTERVALS[interval][0] 2800 }) 2801 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2802 2803 if "code" in responseJSON.keys(): 2804 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2805 2806 else: 2807 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2808 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2809 2810 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2811 2812 blockEnd = blockStart 2813 2814 printCount = len(responseJSONs) # candles to show in console 2815 if responseJSONs: 2816 tempHistory = pd.DataFrame( 2817 data={ 2818 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2819 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2820 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2821 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2822 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2823 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2824 "volume": [int(item["volume"]) for item in responseJSONs], 2825 }, 2826 index=range(len(responseJSONs)), 2827 columns=["date", "time", "open", "high", "low", "close", "volume"], 2828 ) 2829 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2830 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2831 2832 # append only newest candles to old history if --only-missing key present: 2833 if onlyMissing and tempOld is not None and lastTime is not None: 2834 index = 0 # find start index in tempHistory data: 2835 2836 for i, item in tempHistory.iterrows(): 2837 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2838 2839 if curTime == lastTime: 2840 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2841 index = i 2842 printCount = index + 1 2843 break 2844 2845 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2846 2847 else: 2848 history = tempHistory # if no `--only-missing` key then load full data from server 2849 2850 if self.moreDebug: 2851 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2852 2853 if history is not None and not history.empty: 2854 if show and not onlyFiles: 2855 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2856 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2857 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2858 )) 2859 2860 else: 2861 uLogger.warning("Received an empty candles history!") 2862 2863 if self.historyFile is not None: 2864 if history is not None and not history.empty: 2865 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2866 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2867 2868 else: 2869 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2870 2871 else: 2872 if self.moreDebug: 2873 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2874 2875 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2877 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2878 """ 2879 Load candles history from csv-file and return Pandas DataFrame object. 2880 2881 See also: `History()` and `ShowHistoryChart()` methods. 2882 2883 :param filePath: path to csv-file to open. 2884 """ 2885 loadedHistory = None # init candles data object 2886 2887 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2888 2889 if os.path.exists(filePath): 2890 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2891 2892 tfStr = self.priceModel.FormattedDelta( 2893 self.priceModel.timeframe, 2894 "{days} days {hours}h {minutes}m {seconds}s", 2895 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2896 self.priceModel.timeframe, 2897 "{hours}h {minutes}m {seconds}s", 2898 ) 2899 2900 if loadedHistory is not None and not loadedHistory.empty: 2901 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2902 len(loadedHistory), 2903 tfStr, 2904 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2905 ) 2906 2907 else: 2908 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2909 2910 else: 2911 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2912 2913 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2915 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2916 """ 2917 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2918 2919 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2920 Default: `index.html` (both for interact and non-interact candlesticks chart). 2921 2922 See also: `History()` and `LoadHistory()` methods. 2923 2924 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2925 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2926 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2927 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2928 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2929 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2930 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2931 """ 2932 if isinstance(candles, str): 2933 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2934 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2935 2936 elif isinstance(candles, pd.DataFrame): 2937 self.priceModel.prices = candles # set candles chain from variable 2938 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2939 2940 if "datetime" not in candles.columns: 2941 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2942 2943 else: 2944 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2945 raise Exception("Incorrect value") 2946 2947 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2948 2949 if interact: 2950 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2951 2952 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2953 2954 else: 2955 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2956 2957 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2958 2959 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2961 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2962 """ 2963 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2964 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2965 2966 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2967 2968 :param operation: string "Buy" or "Sell". 2969 :param lots: volume, integer count of lots >= 1. 2970 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2971 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2972 :param expDate: string "Undefined" by default or local date in future, 2973 it is a string with format `%Y-%m-%d %H:%M:%S`. 2974 :return: JSON with response from broker server. 2975 """ 2976 if self.accountId is None or not self.accountId: 2977 uLogger.error("Variable `accountId` must be defined for using this method!") 2978 raise Exception("Account ID required") 2979 2980 if operation is None or not operation or operation not in ("Buy", "Sell"): 2981 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2982 raise Exception("Incorrect value") 2983 2984 if lots is None or lots < 1: 2985 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2986 lots = 1 2987 2988 if tp is None or tp < 0: 2989 tp = 0 2990 2991 if sl is None or sl < 0: 2992 sl = 0 2993 2994 if expDate is None or not expDate: 2995 expDate = "Undefined" 2996 2997 if not (self._ticker or self._figi): 2998 uLogger.error("Ticker or FIGI must be defined!") 2999 raise Exception("Ticker or FIGI required") 3000 3001 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3002 self._ticker = instrument["ticker"] 3003 self._figi = instrument["figi"] 3004 3005 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 3006 3007 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3008 self.body = str({ 3009 "figi": self._figi, 3010 "quantity": str(lots), 3011 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3012 "accountId": str(self.accountId), 3013 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3014 }) 3015 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3016 3017 if "orderId" in response.keys(): 3018 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3019 operation, response["orderId"], 3020 self._ticker, self._figi, lots, 3021 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3022 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3023 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3024 )) 3025 3026 if tp > 0: 3027 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3028 3029 if sl > 0: 3030 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3031 3032 else: 3033 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3034 3035 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3037 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3038 """ 3039 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3040 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3041 3042 See also: `Order()` and `Trade()` docstrings. 3043 3044 :param lots: volume, integer count of lots >= 1. 3045 :param tp: float > 0, take profit price of stop-order. 3046 :param sl: float > 0, stop loss price of stop-order. 3047 :param expDate: it's a local date in future. 3048 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3049 :return: JSON with response from broker server. 3050 """ 3051 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3053 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3054 """ 3055 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3056 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3057 3058 See also: `Order()` and `Trade()` docstrings. 3059 3060 :param lots: volume, integer count of lots >= 1. 3061 :param tp: float > 0, take profit price of stop-order. 3062 :param sl: float > 0, stop loss price of stop-order. 3063 :param expDate: it's a local date in the future. 3064 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3065 :return: JSON with response from broker server. 3066 """ 3067 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3069 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3070 """ 3071 Close position of given instruments. 3072 3073 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3074 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3075 This avoids unnecessary downloading data from the server. 3076 """ 3077 if instruments is None or not instruments: 3078 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3079 raise Exception("Ticker or FIGI required") 3080 3081 if isinstance(instruments, str): 3082 instruments = [instruments] 3083 3084 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3085 if uniqueInstruments: 3086 if portfolio is None or not portfolio: 3087 portfolio = self.Overview(show=False) 3088 3089 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3090 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3091 3092 for self._figi in uniqueInstruments: 3093 if self._figi not in allOpened: 3094 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3095 continue 3096 3097 # search open trade info about instrument by ticker: 3098 instrument = {} 3099 for iType in TKS_INSTRUMENTS: 3100 if instrument: 3101 break 3102 3103 for item in portfolio["stat"][iType]: 3104 if item["figi"] == self._figi: 3105 instrument = item 3106 break 3107 3108 if instrument: 3109 self._ticker = instrument["ticker"] 3110 self._figi = instrument["figi"] 3111 3112 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3113 self._ticker, 3114 self._figi, 3115 int(instrument["volume"]), 3116 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3117 )) 3118 3119 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3120 3121 if tradeLots > 0: 3122 if instrument["blocked"] > 0: 3123 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3124 instrument["blocked"], 3125 self._ticker, 3126 tradeLots, 3127 )) 3128 3129 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3130 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3131 3132 else: 3133 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3135 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3136 """ 3137 Close all positions of given instruments with defined type. 3138 3139 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3140 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3141 This avoids unnecessary downloading data from the server. 3142 """ 3143 if iType not in TKS_INSTRUMENTS: 3144 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3145 3146 else: 3147 if portfolio is None or not portfolio: 3148 portfolio = self.Overview(show=False) 3149 3150 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3151 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3152 3153 if tickers and portfolio: 3154 self.CloseTrades(tickers, portfolio) 3155 3156 else: 3157 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3159 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3160 """ 3161 Universal method to create market or limit orders with all available parameters for current `accountId`. 3162 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3163 3164 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3165 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3166 3167 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3168 then broker immediately open market order as you can do simple --buy or --sell operations! 3169 3170 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3171 When current price will go up or down to target price value then broker opens a limit order. 3172 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3173 3174 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3175 3176 :param operation: string "Buy" or "Sell". 3177 :param orderType: string "Limit" or "Stop". 3178 :param lots: volume, integer count of lots >= 1. 3179 :param targetPrice: target price > 0. This is open trade price for limit order. 3180 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3181 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3182 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3183 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3184 Stop loss order always executed by market price. 3185 :param expDate: string "Undefined" by default or local date in future. 3186 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3187 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3188 A limit order has no expiration date, it lasts until the end of the trading day. 3189 :return: JSON with response from broker server. 3190 """ 3191 if self.accountId is None or not self.accountId: 3192 uLogger.error("Variable `accountId` must be defined for using this method!") 3193 raise Exception("Account ID required") 3194 3195 if operation is None or not operation or operation not in ("Buy", "Sell"): 3196 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3197 raise Exception("Incorrect value") 3198 3199 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3200 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3201 raise Exception("Incorrect value") 3202 3203 if lots is None or lots < 1: 3204 uLogger.error("You must define trade volume > 0: integer count of lots!") 3205 raise Exception("Incorrect value") 3206 3207 if targetPrice is None or targetPrice <= 0: 3208 uLogger.error("Target price for limit-order must be greater than 0!") 3209 raise Exception("Incorrect value") 3210 3211 if limitPrice is None or limitPrice <= 0: 3212 limitPrice = targetPrice 3213 3214 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3215 stopType = "Limit" 3216 3217 if expDate is None or not expDate: 3218 expDate = "Undefined" 3219 3220 if not (self._ticker or self._figi): 3221 uLogger.error("Tocker or FIGI must be defined!") 3222 raise Exception("Ticker or FIGI required") 3223 3224 response = {} 3225 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3226 self._ticker = instrument["ticker"] 3227 self._figi = instrument["figi"] 3228 3229 if orderType == "Limit": 3230 uLogger.debug( 3231 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3232 self._ticker, self._figi, 3233 operation, lots, targetPrice, instrument["currency"], 3234 )) 3235 3236 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3237 self.body = str({ 3238 "figi": self._figi, 3239 "quantity": str(lots), 3240 "price": FloatToNano(targetPrice), 3241 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3242 "accountId": str(self.accountId), 3243 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3244 }) 3245 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3246 3247 if "orderId" in response.keys(): 3248 uLogger.info( 3249 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3250 response["orderId"], self._ticker, self._figi, operation, lots, 3251 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3252 )) 3253 3254 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3255 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3256 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3257 targetPrice, instrument["currency"], 3258 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3259 )) 3260 3261 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3262 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3263 targetPrice, instrument["currency"], 3264 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3265 )) 3266 3267 else: 3268 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3269 3270 if orderType == "Stop": 3271 uLogger.debug( 3272 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3273 self._ticker, self._figi, 3274 operation, lots, 3275 targetPrice, instrument["currency"], 3276 limitPrice, instrument["currency"], 3277 stopType, expDate, 3278 )) 3279 3280 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3281 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3282 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3283 3284 body = { 3285 "figi": self._figi, 3286 "quantity": str(lots), 3287 "price": FloatToNano(limitPrice), 3288 "stopPrice": FloatToNano(targetPrice), 3289 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3290 "accountId": str(self.accountId), 3291 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3292 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3293 } 3294 3295 if expDateUTC: 3296 body["expireDate"] = expDateUTC 3297 3298 self.body = str(body) 3299 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3300 3301 if "stopOrderId" in response.keys(): 3302 uLogger.info( 3303 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3304 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3305 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3306 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3307 TKS_STOP_ORDER_TYPES[stopOrderType], 3308 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3309 )) 3310 3311 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3312 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3313 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3314 targetPrice, instrument["currency"], 3315 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3316 )) 3317 3318 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3319 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3320 targetPrice, instrument["currency"], 3321 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3322 )) 3323 3324 else: 3325 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3326 3327 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3329 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3330 """ 3331 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3332 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3333 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3334 See also: `Order()` docstring. 3335 3336 :param lots: volume, integer count of lots >= 1. 3337 :param targetPrice: target price > 0. This is open trade price for limit order. 3338 :return: JSON with response from broker server. 3339 """ 3340 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3342 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3343 """ 3344 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3345 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3346 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3347 target price value then broker opens a limit order. See also: `Order()` docstring. 3348 3349 :param lots: volume, integer count of lots >= 1. 3350 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3351 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3352 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3353 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3354 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3355 :param expDate: string "Undefined" by default or local date in future. 3356 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3357 This date is converting to UTC format for server. 3358 :return: JSON with response from broker server. 3359 """ 3360 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3362 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3363 """ 3364 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3365 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3366 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3367 See also: `Order()` docstring. 3368 3369 :param lots: volume, integer count of lots >= 1. 3370 :param targetPrice: target price > 0. This is open trade price for limit order. 3371 :return: JSON with response from broker server. 3372 """ 3373 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3375 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3376 """ 3377 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3378 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3379 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3380 target price value then broker opens a limit order. See also: `Order()` docstring. 3381 3382 :param lots: volume, integer count of lots >= 1. 3383 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3384 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3385 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3386 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3387 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3388 :param expDate: string "Undefined" by default or local date in future. 3389 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3390 This date is converting to UTC format for server. 3391 :return: JSON with response from broker server. 3392 """ 3393 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3395 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3396 """ 3397 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3398 3399 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3400 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3401 This avoids unnecessary downloading data from the server. 3402 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3403 """ 3404 if self.accountId is None or not self.accountId: 3405 uLogger.error("Variable `accountId` must be defined for using this method!") 3406 raise Exception("Account ID required") 3407 3408 if orderIDs: 3409 if allOrdersIDs is None: 3410 rawOrders = self.RequestPendingOrders() 3411 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3412 3413 if allStopOrdersIDs is None: 3414 rawStopOrders = self.RequestStopOrders() 3415 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3416 3417 for orderID in orderIDs: 3418 idInPendingOrders = orderID in allOrdersIDs 3419 idInStopOrders = orderID in allStopOrdersIDs 3420 3421 if not (idInPendingOrders or idInStopOrders): 3422 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3423 continue 3424 3425 else: 3426 if idInPendingOrders: 3427 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3428 3429 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3430 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3431 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3432 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3433 3434 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3435 if self.moreDebug: 3436 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3437 3438 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3439 3440 else: 3441 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3442 3443 elif idInStopOrders: 3444 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3445 3446 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3447 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3448 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3449 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3450 3451 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3452 if self.moreDebug: 3453 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3454 3455 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3456 3457 else: 3458 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3459 3460 else: 3461 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3463 def CloseAllOrders(self) -> None: 3464 """ 3465 Gets a list of open pending and stop orders and cancel it all. 3466 """ 3467 rawOrders = self.RequestPendingOrders() 3468 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3469 lenOrders = len(allOrdersIDs) 3470 3471 rawStopOrders = self.RequestStopOrders() 3472 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3473 lenSOrders = len(allStopOrdersIDs) 3474 3475 if lenOrders > 0 or lenSOrders > 0: 3476 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3477 3478 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3479 3480 else: 3481 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3483 def CloseAll(self, *args) -> None: 3484 """ 3485 Close all available (not blocked) opened trades and orders. 3486 3487 Also, you can select one or more keywords case-insensitive: 3488 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3489 3490 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3491 """ 3492 overview = self.Overview(show=False) # get all open trades info 3493 3494 if len(args) == 0: 3495 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3496 self.CloseAllOrders() # close all pending and stop orders 3497 3498 for iType in TKS_INSTRUMENTS: 3499 if iType != "Currencies": 3500 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3501 3502 else: 3503 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3504 lowerArgs = [x.lower() for x in args] 3505 3506 if "orders" in lowerArgs: 3507 self.CloseAllOrders() # close all pending and stop orders 3508 3509 for iType in TKS_INSTRUMENTS: 3510 if iType.lower() in lowerArgs and iType != "Currencies": 3511 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3513 def CloseAllByTicker(self, instrument: str) -> None: 3514 """ 3515 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3516 3517 This method searches opened trade and orders of instrument throw all portfolio and then use 3518 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3519 3520 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3521 3522 :param instrument: string with ticker. 3523 """ 3524 if instrument is None or not instrument: 3525 uLogger.error("Ticker name must be defined for using this method!") 3526 raise Exception("Ticker required") 3527 3528 overview = self.Overview(show=False) # get user portfolio with all open trades info 3529 3530 self._ticker = instrument # try to set instrument as ticker 3531 self._figi = "" 3532 3533 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3534 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3535 3536 if limitAll and self.IsInLimitOrders(portfolio=overview): 3537 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3538 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3539 3540 if stopAll and self.IsInStopOrders(portfolio=overview): 3541 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3542 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3543 3544 if self.IsInPortfolio(portfolio=overview): 3545 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3546 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with ticker.
3548 def CloseAllByFIGI(self, instrument: str) -> None: 3549 """ 3550 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3551 3552 This method searches opened trade and orders of instrument throw all portfolio and then use 3553 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3554 3555 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3556 3557 :param instrument: string with FIGI id. 3558 """ 3559 if instrument is None or not instrument: 3560 uLogger.error("FIGI id must be defined for using this method!") 3561 raise Exception("FIGI required") 3562 3563 overview = self.Overview(show=False) # get user portfolio with all open trades info 3564 3565 self._ticker = "" 3566 self._figi = instrument # try to set instrument as FIGI id 3567 3568 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3569 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3570 3571 if limitAll and self.IsInLimitOrders(portfolio=overview): 3572 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3573 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3574 3575 if stopAll and self.IsInStopOrders(portfolio=overview): 3576 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3577 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3578 3579 if self.IsInPortfolio(portfolio=overview): 3580 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3581 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with FIGI id.
3583 @staticmethod 3584 def ParseOrderParameters(operation, **inputParameters): 3585 """ 3586 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3587 3588 :param operation: string "Buy" or "Sell". 3589 :param inputParameters: this is dict of strings that looks like this 3590 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3591 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3592 "prices" key: one or more prices to open limit-orders 3593 Counts of values in lots and prices lists must be equals! 3594 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3595 """ 3596 # TODO: update order grid work with api v2 3597 pass 3598 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3599 # 3600 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3601 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3602 # raise Exception("Incorrect value") 3603 # 3604 # if "l" in inputParameters.keys(): 3605 # inputParameters["lots"] = inputParameters.pop("l") 3606 # 3607 # if "p" in inputParameters.keys(): 3608 # inputParameters["prices"] = inputParameters.pop("p") 3609 # 3610 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3611 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3612 # raise Exception("Incorrect value") 3613 # 3614 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3615 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3616 # 3617 # if len(lots) != len(prices): 3618 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3619 # raise Exception("Incorrect value") 3620 # 3621 # uLogger.debug("Extracted parameters for orders:") 3622 # uLogger.debug("lots = {}".format(lots)) 3623 # uLogger.debug("prices = {}".format(prices)) 3624 # 3625 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3626 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3627 # uLogger.debug("Order parameters: {}".format(result)) 3628 # 3629 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3631 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3632 """ 3633 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3634 3635 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3636 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3637 """ 3638 result = False 3639 msg = "Instrument not defined!" 3640 3641 if portfolio is None or not portfolio: 3642 portfolio = self.Overview(show=False) 3643 3644 if self._ticker: 3645 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3646 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3647 3648 for iType in TKS_INSTRUMENTS: 3649 for instrument in portfolio["stat"][iType]: 3650 if instrument["ticker"] == self._ticker: 3651 result = True 3652 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3653 break 3654 3655 elif self._figi: 3656 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3657 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3658 3659 for iType in TKS_INSTRUMENTS: 3660 for instrument in portfolio["stat"][iType]: 3661 if instrument["figi"] == self._figi: 3662 result = True 3663 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3664 break 3665 3666 else: 3667 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3668 3669 uLogger.debug(msg) 3670 3671 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3673 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3674 """ 3675 Returns instrument from the user's portfolio if it presents there. 3676 Instrument must be defined by `ticker` (highly priority) or `figi`. 3677 3678 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3679 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3680 """ 3681 result = None 3682 msg = "Instrument not defined!" 3683 3684 if portfolio is None or not portfolio: 3685 portfolio = self.Overview(show=False) 3686 3687 if self._ticker: 3688 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3689 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3690 3691 for iType in TKS_INSTRUMENTS: 3692 for instrument in portfolio["stat"][iType]: 3693 if instrument["ticker"] == self._ticker: 3694 result = instrument 3695 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3696 break 3697 3698 elif self._figi: 3699 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3700 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3701 3702 for iType in TKS_INSTRUMENTS: 3703 for instrument in portfolio["stat"][iType]: 3704 if instrument["figi"] == self._figi: 3705 result = instrument 3706 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3707 break 3708 3709 else: 3710 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3711 3712 uLogger.debug(msg) 3713 3714 return result
Returns instrument from the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3716 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3717 """ 3718 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3719 3720 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3721 3722 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3723 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3724 """ 3725 result = False 3726 msg = "Instrument not defined!" 3727 3728 if portfolio is None or not portfolio: 3729 portfolio = self.Overview(show=False) 3730 3731 if self._ticker: 3732 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3733 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3734 3735 for instrument in portfolio["stat"]["orders"]: 3736 if instrument["ticker"] == self._ticker: 3737 result = True 3738 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3739 break 3740 3741 elif self._figi: 3742 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3743 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3744 3745 for instrument in portfolio["stat"]["orders"]: 3746 if instrument["figi"] == self._figi: 3747 result = True 3748 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3749 break 3750 3751 else: 3752 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3753 3754 uLogger.debug(msg) 3755 3756 return result
Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif limit orders list contains some limit orders for the instrument,Falseotherwise.
3758 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3759 """ 3760 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3761 Instrument must be defined by `ticker` (highly priority) or `figi`. 3762 3763 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3764 3765 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3766 :return: list with `orderID`s of limit orders. 3767 """ 3768 result = [] 3769 msg = "Instrument not defined!" 3770 3771 if portfolio is None or not portfolio: 3772 portfolio = self.Overview(show=False) 3773 3774 if self._ticker: 3775 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3776 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3777 3778 for instrument in portfolio["stat"]["orders"]: 3779 if instrument["ticker"] == self._ticker: 3780 result.append(instrument["orderID"]) 3781 3782 if result: 3783 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3784 3785 elif self._figi: 3786 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3787 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3788 3789 for instrument in portfolio["stat"]["orders"]: 3790 if instrument["figi"] == self._figi: 3791 result.append(instrument["orderID"]) 3792 3793 if result: 3794 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3795 3796 else: 3797 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3798 3799 uLogger.debug(msg) 3800 3801 return result
Returns list with all orderIDs of opened pending limit orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of limit orders.
3803 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3804 """ 3805 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3806 3807 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3808 3809 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3810 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3811 """ 3812 result = False 3813 msg = "Instrument not defined!" 3814 3815 if portfolio is None or not portfolio: 3816 portfolio = self.Overview(show=False) 3817 3818 if self._ticker: 3819 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3820 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3821 3822 for instrument in portfolio["stat"]["stopOrders"]: 3823 if instrument["ticker"] == self._ticker: 3824 result = True 3825 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3826 break 3827 3828 elif self._figi: 3829 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3830 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3831 3832 for instrument in portfolio["stat"]["stopOrders"]: 3833 if instrument["figi"] == self._figi: 3834 result = True 3835 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3836 break 3837 3838 else: 3839 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3840 3841 uLogger.debug(msg) 3842 3843 return result
Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif stop orders list contains some stop orders for the instrument,Falseotherwise.
3845 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3846 """ 3847 Returns list with all `orderID`s of opened stop orders for the instrument. 3848 Instrument must be defined by `ticker` (highly priority) or `figi`. 3849 3850 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3851 3852 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3853 :return: list with `orderID`s of stop orders. 3854 """ 3855 result = [] 3856 msg = "Instrument not defined!" 3857 3858 if portfolio is None or not portfolio: 3859 portfolio = self.Overview(show=False) 3860 3861 if self._ticker: 3862 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3863 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3864 3865 for instrument in portfolio["stat"]["stopOrders"]: 3866 if instrument["ticker"] == self._ticker: 3867 result.append(instrument["orderID"]) 3868 3869 if result: 3870 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3871 3872 elif self._figi: 3873 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3874 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3875 3876 for instrument in portfolio["stat"]["stopOrders"]: 3877 if instrument["figi"] == self._figi: 3878 result.append(instrument["orderID"]) 3879 3880 if result: 3881 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3882 3883 else: 3884 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3885 3886 uLogger.debug(msg) 3887 3888 return result
Returns list with all orderIDs of opened stop orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of stop orders.
3890 def RequestLimits(self) -> dict: 3891 """ 3892 Method for obtaining the available funds for withdrawal for current `accountId`. 3893 3894 See also: 3895 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3896 - `OverviewLimits()` method 3897 3898 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3899 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3900 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3901 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3902 """ 3903 if self.accountId is None or not self.accountId: 3904 uLogger.error("Variable `accountId` must be defined for using this method!") 3905 raise Exception("Account ID required") 3906 3907 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3908 3909 self.body = str({"accountId": self.accountId}) 3910 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3911 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3912 3913 if self.moreDebug: 3914 uLogger.debug("Records about available funds for withdrawal successfully received") 3915 3916 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3918 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3919 """ 3920 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3921 3922 See also: `RequestLimits()`. 3923 3924 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3925 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3926 :return: dict with raw parsed data from server and some calculated statistics about it. 3927 """ 3928 if self.accountId is None or not self.accountId: 3929 uLogger.error("Variable `accountId` must be defined for using this method!") 3930 raise Exception("Account ID required") 3931 3932 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3933 3934 view = { 3935 "rawLimits": rawLimits, 3936 "limits": { # parsed data for every currency: 3937 "money": { # this is an array of portfolio currency positions 3938 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3939 }, 3940 "blocked": { # this is an array of blocked currency 3941 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3942 }, 3943 "blockedGuarantee": { # this is locked money under collateral for futures 3944 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3945 }, 3946 }, 3947 } 3948 3949 # --- Prepare text table with limits in human-readable format: 3950 if show or onlyFiles: 3951 info = [ 3952 "# Withdrawal limits\n\n", 3953 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3954 "* **Account ID:** [{}]\n".format(self.accountId), 3955 ] 3956 3957 if view["limits"]["money"]: 3958 info.extend([ 3959 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3960 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3961 ]) 3962 3963 else: 3964 info.append("\nNo withdrawal limits\n") 3965 3966 for curr in view["limits"]["money"].keys(): 3967 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3968 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3969 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3970 3971 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3972 "[{}]".format(curr), 3973 "{:.2f}".format(view["limits"]["money"][curr]), 3974 "{:.2f}".format(availableMoney), 3975 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3976 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3977 ) 3978 3979 if curr == "rub": 3980 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3981 3982 else: 3983 info.append(infoStr) 3984 3985 infoText = "".join(info) 3986 3987 if show and not onlyFiles: 3988 uLogger.info(infoText) 3989 3990 if self.withdrawalLimitsFile and (show or onlyFiles): 3991 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3992 fH.write(infoText) 3993 3994 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3995 3996 if self.useHTMLReports: 3997 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3998 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3999 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 4000 4001 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4002 4003 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4005 def RequestAccounts(self) -> dict: 4006 """ 4007 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4008 4009 See also: 4010 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4011 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4012 - `OverviewUserInfo()` method 4013 4014 :return: dict with raw data from server that contains accounts info. Example of dict: 4015 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4016 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4017 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4018 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4019 """ 4020 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4021 4022 self.body = str({}) 4023 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4024 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4025 4026 if self.moreDebug: 4027 uLogger.debug("Records about available accounts successfully received") 4028 4029 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
4031 def RequestUserInfo(self) -> dict: 4032 """ 4033 Method for requesting common user's information. 4034 4035 See also: 4036 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4037 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4038 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4039 - `OverviewUserInfo()` method 4040 4041 :return: dict with raw data from server that contains user's information. Example of dict: 4042 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4043 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4044 """ 4045 uLogger.debug("Requesting common user's information. Wait, please...") 4046 4047 self.body = str({}) 4048 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4049 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4050 4051 if self.moreDebug: 4052 uLogger.debug("Records about current user successfully received") 4053 4054 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
4056 def RequestMarginStatus(self, accountId: str = None) -> dict: 4057 """ 4058 Method for requesting margin calculation for defined account ID. 4059 4060 See also: 4061 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4062 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4063 - `OverviewUserInfo()` method 4064 4065 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4066 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4067 Example of responses: 4068 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4069 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4070 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4071 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4072 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4073 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4074 """ 4075 if accountId is None or not accountId: 4076 if self.accountId is None or not self.accountId: 4077 uLogger.error("Variable `accountId` must be defined for using this method!") 4078 raise Exception("Account ID required") 4079 4080 else: 4081 accountId = self.accountId # use `self.accountId` (main ID) by default 4082 4083 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4084 4085 self.body = str({"accountId": accountId}) 4086 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4087 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4088 4089 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4090 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4091 rawMargin = {} 4092 4093 else: 4094 if self.moreDebug: 4095 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4096 4097 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
4099 def RequestTariffLimits(self) -> dict: 4100 """ 4101 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4102 4103 See also: 4104 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4105 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4106 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4107 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4108 - `OverviewUserInfo()` method 4109 4110 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4111 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4112 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4113 """ 4114 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4115 4116 self.body = str({}) 4117 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4118 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4119 4120 if self.moreDebug: 4121 uLogger.debug("Records with limits of current tariff successfully received") 4122 4123 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
4125 def RequestBondCoupons(self, iJSON: dict) -> dict: 4126 """ 4127 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4128 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4129 All dates are in UTC timezone. 4130 4131 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4132 Documentation: 4133 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4134 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4135 4136 See also: `ExtendBondsData()`. 4137 4138 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4139 If raw iJSON is not data of bond then server returns an error [400] with message: 4140 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4141 :return: dictionary with bond payment calendar. Response example 4142 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4143 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4144 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4145 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4146 """ 4147 if iJSON["figi"] is None or not iJSON["figi"]: 4148 uLogger.error("FIGI must be defined for using this method!") 4149 raise Exception("FIGI required") 4150 4151 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4152 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4153 4154 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4155 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4156 self._figi, 4157 startDate, 4158 endDate, 4159 )) 4160 4161 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4162 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4163 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4164 4165 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4166 uLogger.warning("Instrument type is not bond!") 4167 4168 else: 4169 if self.moreDebug: 4170 uLogger.debug("Records about bond payment calendar successfully received") 4171 4172 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self._ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
4174 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4175 """ 4176 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4177 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4178 coupon yields, current yields and some statistics etc. 4179 4180 WARNING! This is too long operation if a lot of bonds requested from broker server. 4181 4182 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4183 4184 :param instruments: list of strings with tickers or FIGIs. 4185 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4186 for further used by data scientists or stock analytics. 4187 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4188 In XLSX-file and Pandas DataFrame fields mean: 4189 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4190 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4191 """ 4192 if instruments is None or not instruments: 4193 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4194 raise Exception("Ticker or FIGI required") 4195 4196 if isinstance(instruments, str): 4197 instruments = [instruments] 4198 4199 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4200 4201 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4202 4203 iCount = len(uniqueInstruments) 4204 tooLong = iCount >= 20 4205 if tooLong: 4206 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4207 4208 bonds = None 4209 for i, self._figi in enumerate(uniqueInstruments): 4210 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4211 4212 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4213 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4214 rawBond = self.SearchByFIGI(requestPrice=True) 4215 4216 # Widen raw data with UTC current time (iData["actualDateTime"]): 4217 actualDate = datetime.now(tzutc()) 4218 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4219 4220 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4221 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4222 4223 # Replace some values with human-readable: 4224 iData["nominalCurrency"] = iData["nominal"]["currency"] 4225 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4226 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4227 iData["aciCurrency"] = iData["aciValue"]["currency"] 4228 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4229 iData["issueSize"] = int(iData["issueSize"]) 4230 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4231 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4232 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4233 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4234 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4235 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4236 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4237 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4238 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4239 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4240 4241 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4242 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4243 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4244 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4245 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4246 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4247 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4248 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4249 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4250 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4251 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4252 4253 # Widen raw data with calendar data from `rawCalendar` values: 4254 calendarData = [] 4255 if "events" in iData["rawCalendar"].keys(): 4256 for item in iData["rawCalendar"]["events"]: 4257 calendarData.append({ 4258 "couponDate": item["couponDate"], 4259 "couponNumber": int(item["couponNumber"]), 4260 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4261 "payCurrency": item["payOneBond"]["currency"], 4262 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4263 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4264 "couponStartDate": item["couponStartDate"], 4265 "couponEndDate": item["couponEndDate"], 4266 "couponPeriod": item["couponPeriod"], 4267 }) 4268 4269 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4270 if "maturityDate" not in iData.keys(): 4271 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4272 4273 # Widen raw data with Coupon Rate. 4274 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4275 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4276 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4277 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4278 4279 # Widen raw data with Yield to Maturity (YTM) on current date. 4280 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4281 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4282 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4283 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4284 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4285 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4286 4287 iData["calendar"] = calendarData # adds calendar at the end 4288 4289 # Remove not used data: 4290 iData.pop("uid") 4291 iData.pop("positionUid") 4292 iData.pop("currentPrice") 4293 iData.pop("rawCalendar") 4294 4295 colNames = list(iData.keys()) 4296 if bonds is None: 4297 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4298 4299 else: 4300 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4301 4302 else: 4303 uLogger.warning("Instrument is not a bond!") 4304 4305 processed = round(100 * (i + 1) / iCount, 1) 4306 if tooLong and processed % 5 == 0: 4307 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4308 4309 else: 4310 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4311 4312 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4313 4314 # Saving bonds from Pandas DataFrame to XLSX sheet: 4315 if xlsx and self.bondsXLSXFile: 4316 with pd.ExcelWriter( 4317 path=self.bondsXLSXFile, 4318 date_format=TKS_DATE_FORMAT, 4319 datetime_format=TKS_DATE_TIME_FORMAT, 4320 mode="w", 4321 ) as writer: 4322 bonds.to_excel( 4323 writer, 4324 sheet_name="Extended bonds data", 4325 index=True, 4326 encoding="UTF-8", 4327 freeze_panes=(1, 1), 4328 ) # saving as XLSX-file with freeze first row and column as headers 4329 4330 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4331 4332 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4334 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4335 """ 4336 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4337 4338 WARNING! This is too long operation if a lot of bonds requested from broker server. 4339 4340 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4341 4342 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4343 extended information about bonds: main info, current prices, bond payment calendar, 4344 coupon yields, current yields and some statistics etc. 4345 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4346 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4347 for further used by data scientists or stock analytics. 4348 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4349 """ 4350 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4351 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4352 4353 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4354 4355 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4356 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4357 calendar = None 4358 for bond in extBonds.iterrows(): 4359 for item in bond[1]["calendar"]: 4360 cData = { 4361 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4362 "couponDate": item["couponDate"], 4363 "figi": bond[1]["figi"], 4364 "ticker": bond[1]["ticker"], 4365 "name": bond[1]["name"], 4366 "couponNumber": item["couponNumber"], 4367 "payOneBond": item["payOneBond"], 4368 "payCurrency": item["payCurrency"], 4369 "couponType": item["couponType"], 4370 "couponPeriod": item["couponPeriod"], 4371 "fixDate": item["fixDate"], 4372 "couponStartDate": item["couponStartDate"], 4373 "couponEndDate": item["couponEndDate"], 4374 } 4375 4376 if calendar is None: 4377 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4378 4379 else: 4380 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4381 4382 if calendar is not None: 4383 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4384 4385 # Saving calendar from Pandas DataFrame to XLSX sheet: 4386 if xlsx: 4387 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4388 4389 with pd.ExcelWriter( 4390 path=xlsxCalendarFile, 4391 date_format=TKS_DATE_FORMAT, 4392 datetime_format=TKS_DATE_TIME_FORMAT, 4393 mode="w", 4394 ) as writer: 4395 humanReadable = calendar.copy(deep=True) 4396 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4397 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4398 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4399 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4400 humanReadable.columns = colNames # human-readable column names 4401 4402 humanReadable.to_excel( 4403 writer, 4404 sheet_name="Bond payments calendar", 4405 index=False, 4406 encoding="UTF-8", 4407 freeze_panes=(1, 2), 4408 ) # saving as XLSX-file with freeze first row and column as headers 4409 4410 del humanReadable # release df in memory 4411 4412 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4413 4414 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4416 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4417 """ 4418 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4419 Also, creates Markdown file with calendar data, `calendar.md` by default. 4420 4421 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4422 4423 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4424 extended information about bonds: main info, current prices, bond payment calendar, 4425 coupon yields, current yields and some statistics etc. 4426 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4427 :param show: if `True` then also printing bonds payment calendar to the console, 4428 otherwise save to file `calendarFile` only. `False` by default. 4429 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4430 :return: multilines text in Markdown format with bonds payment calendar as a table. 4431 """ 4432 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4433 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4434 4435 infoText = "# Bond payments calendar\n\n" 4436 4437 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4438 4439 if not (calendar is None or calendar.empty): 4440 splitLine = "| | | | | | | | | |\n" 4441 4442 info = [ 4443 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4444 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4445 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4446 ] 4447 4448 newMonth = False 4449 notOneBond = calendar["figi"].nunique() > 1 4450 for i, bond in enumerate(calendar.iterrows()): 4451 if newMonth and notOneBond: 4452 info.append(splitLine) 4453 4454 info.append( 4455 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4456 " √" if bond[1]["paid"] else " —", 4457 bond[1]["couponDate"].split("T")[0], 4458 bond[1]["figi"], 4459 bond[1]["ticker"], 4460 bond[1]["couponNumber"], 4461 "{} {}".format( 4462 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4463 bond[1]["payCurrency"], 4464 ), 4465 bond[1]["couponType"], 4466 bond[1]["couponPeriod"], 4467 bond[1]["fixDate"].split("T")[0], 4468 ) 4469 ) 4470 4471 if i < len(calendar.values) - 1: 4472 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4473 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4474 newMonth = False if curDate.month == nextDate.month else True 4475 4476 else: 4477 newMonth = False 4478 4479 infoText += "".join(info) 4480 4481 if show and not onlyFiles: 4482 uLogger.info("{}".format(infoText)) 4483 4484 if self.calendarFile is not None and (show or onlyFiles): 4485 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4486 fH.write(infoText) 4487 4488 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4489 4490 if self.useHTMLReports: 4491 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4492 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4493 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4494 4495 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4496 4497 else: 4498 infoText += "No data\n" 4499 4500 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4502 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4503 """ 4504 Method for parsing and show simple table with all available user accounts. 4505 4506 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4507 4508 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4509 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4510 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4511 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4512 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4513 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4514 "closed": "—", "access": "Full access" }, ...}}` 4515 """ 4516 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4517 4518 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4519 accounts = { 4520 item["id"]: { 4521 "type": TKS_ACCOUNT_TYPES[item["type"]], 4522 "name": item["name"], 4523 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4524 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4525 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4526 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4527 } for item in rawAccounts["accounts"] 4528 } 4529 4530 # Raw and parsed data with some fields replaced in "stat" section: 4531 view = { 4532 "rawAccounts": rawAccounts, 4533 "stat": accounts, 4534 } 4535 4536 # --- Prepare simple text table with only accounts data in human-readable format: 4537 if show or onlyFiles: 4538 info = [ 4539 "# User accounts\n\n", 4540 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4541 "| Account ID | Type | Status | Name |\n", 4542 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4543 ] 4544 4545 for account in view["stat"].keys(): 4546 info.extend([ 4547 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4548 account, 4549 view["stat"][account]["type"], 4550 view["stat"][account]["status"], 4551 view["stat"][account]["name"], 4552 ) 4553 ]) 4554 4555 infoText = "".join(info) 4556 4557 if show and not onlyFiles: 4558 uLogger.info(infoText) 4559 4560 if self.userAccountsFile and (show or onlyFiles): 4561 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4562 fH.write(infoText) 4563 4564 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4565 4566 if self.useHTMLReports: 4567 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4568 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4569 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4570 4571 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4572 4573 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4575 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4576 """ 4577 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4578 4579 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4580 4581 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4582 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4583 :return: dict with raw parsed data from server and some calculated statistics about it. 4584 """ 4585 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4586 tmpTicker = self._ticker 4587 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4588 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4589 self._ticker = tmpTicker 4590 4591 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4592 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4593 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4594 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4595 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4596 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4597 4598 # This is dict with parsed common user data: 4599 userInfo = { 4600 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4601 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4602 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4603 "tariff": rawUserInfo["tariff"], 4604 } 4605 4606 # This is an array of dict with parsed margin statuses for every account IDs: 4607 margins = {} 4608 for accountId in accounts.keys(): 4609 if rawMargins[accountId]: 4610 margins[accountId] = { 4611 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4612 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4613 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4614 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4615 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4616 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4617 "missing": missing["volume"], 4618 } 4619 4620 else: 4621 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4622 4623 unary = {} # unary-connection limits 4624 for item in rawTariffLimits["unaryLimits"]: 4625 if item["limitPerMinute"] in unary.keys(): 4626 unary[item["limitPerMinute"]].extend(item["methods"]) 4627 4628 else: 4629 unary[item["limitPerMinute"]] = item["methods"] 4630 4631 stream = {} # stream-connection limits 4632 for item in rawTariffLimits["streamLimits"]: 4633 if item["limit"] in stream.keys(): 4634 stream[item["limit"]].extend(item["streams"]) 4635 4636 else: 4637 stream[item["limit"]] = item["streams"] 4638 4639 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4640 limits = { 4641 "unary": unary, 4642 "stream": stream, 4643 } 4644 4645 # Raw and parsed data as an output result: 4646 view = { 4647 "rawUserInfo": rawUserInfo, 4648 "rawAccounts": rawAccounts, 4649 "rawMargins": rawMargins, 4650 "rawTariffLimits": rawTariffLimits, 4651 "stat": { 4652 "overview": overview, 4653 "userInfo": userInfo, 4654 "accounts": accounts, 4655 "margins": margins, 4656 "limits": limits, 4657 }, 4658 } 4659 4660 # --- Prepare text table with user information in human-readable format: 4661 if show or onlyFiles: 4662 info = [ 4663 "# Full user information\n\n", 4664 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4665 "## Common information\n\n", 4666 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4667 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4668 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4669 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4670 "\n## User accounts\n\n", 4671 ] 4672 4673 for account in view["stat"]["accounts"].keys(): 4674 info.extend([ 4675 "### ID: [{}]\n\n".format(account), 4676 "| Parameters | Values |\n", 4677 "|----------------------|--------------------------------------------------------------|\n", 4678 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4679 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4680 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4681 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4682 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4683 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4684 ]) 4685 4686 if margins[account]: 4687 info.extend([ 4688 "| Margin status: | Enabled |\n", 4689 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4690 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4691 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4692 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4693 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4694 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4695 ]) 4696 4697 else: 4698 info.append("| Margin status: | Disabled |\n\n") 4699 4700 info.extend([ 4701 "\n## Current user tariff limits\n", 4702 "\n### See also\n", 4703 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4704 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4705 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4706 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4707 "\n### Unary limits\n", 4708 ]) 4709 4710 if unary: 4711 for key, values in sorted(unary.items()): 4712 info.append("\n* Max requests per minute: {}\n".format(key)) 4713 4714 for value in values: 4715 info.append(" - {}\n".format(value)) 4716 4717 else: 4718 info.append("\nNot available\n") 4719 4720 info.append("\n### Stream limits\n") 4721 4722 if stream: 4723 for key, values in sorted(stream.items()): 4724 info.append("\n* Max stream connections: {}\n".format(key)) 4725 4726 for value in values: 4727 info.append(" - {}\n".format(value)) 4728 4729 else: 4730 info.append("\nNot available\n") 4731 4732 infoText = "".join(info) 4733 4734 if show and not onlyFiles: 4735 uLogger.info(infoText) 4736 4737 if self.userInfoFile and (show or onlyFiles): 4738 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4739 fH.write(infoText) 4740 4741 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4742 4743 if self.useHTMLReports: 4744 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4745 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4746 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4747 4748 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4749 4750 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4753class Args: 4754 """ 4755 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4756 """ 4757 def __init__(self, **kwargs): 4758 self.__dict__.update(kwargs) 4759 4760 def __getattr__(self, item): 4761 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4764def ParseArgs(): 4765 """This function get and parse command line keys.""" 4766 parser = ArgumentParser() # command-line string parser 4767 4768 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4769 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4770 4771 # --- options: 4772 4773 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4774 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4775 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4776 4777 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4778 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4779 4780 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4781 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4782 4783 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4784 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4785 4786 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4787 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4788 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4789 4790 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4791 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4792 4793 # --- commands: 4794 4795 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4796 4797 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4798 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4799 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4800 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4801 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4802 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4803 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4804 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4805 4806 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4807 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4808 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4809 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4810 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4811 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4812 4813 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4814 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4815 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4816 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4817 4818 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4819 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4820 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4821 4822 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4823 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4824 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4825 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4826 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4827 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4828 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4829 4830 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4831 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4832 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4833 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4834 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4835 4836 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4837 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4838 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4839 4840 cmdArgs = parser.parse_args() 4841 return cmdArgs
This function get and parse command line keys.
4844def Main(**kwargs): 4845 """ 4846 Main function for work with TKSBrokerAPI in the console. 4847 4848 See examples: 4849 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4850 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4851 """ 4852 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4853 4854 if args.debug_level: 4855 uLogger.level = 10 # always debug level by default 4856 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4857 4858 exitCode = 0 4859 start = datetime.now(tzutc()) 4860 uLogger.debug("=-" * 50) 4861 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4862 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4863 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4864 )) 4865 4866 # trying to calculate full current version: 4867 buildVersion = __version__ 4868 try: 4869 v = version("tksbrokerapi") 4870 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4871 4872 except Exception: 4873 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4874 4875 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4876 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4877 4878 try: 4879 if args.version: 4880 print("TKSBrokerAPI {}".format(buildVersion)) 4881 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4882 4883 else: 4884 # Init class for trading with Tinkoff Broker: 4885 trader = TinkoffBrokerServer( 4886 token=args.token, 4887 accountId=args.account_id, 4888 useCache=not args.no_cache, 4889 ) 4890 4891 # --- set some options: 4892 4893 if args.more: 4894 trader.moreDebug = True 4895 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4896 4897 if args.html: 4898 trader.useHTMLReports = True 4899 4900 if args.ticker: 4901 ticker = str(args.ticker).upper() # Tickers may be upper case only 4902 4903 if ticker in trader.aliasesKeys: 4904 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4905 4906 else: 4907 trader.ticker = ticker 4908 4909 if args.figi: 4910 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4911 4912 if args.depth is not None: 4913 trader.depth = args.depth 4914 4915 # --- do one command: 4916 4917 if args.list: 4918 if args.output is not None: 4919 trader.instrumentsFile = args.output 4920 4921 trader.ShowInstrumentsInfo(show=True) 4922 4923 elif args.list_xlsx: 4924 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4925 4926 elif args.bonds_xlsx is not None: 4927 if args.output is not None: 4928 trader.bondsXLSXFile = args.output 4929 4930 if len(args.bonds_xlsx) == 0: 4931 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4932 4933 else: 4934 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4935 4936 elif args.search: 4937 if args.output is not None: 4938 trader.searchResultsFile = args.output 4939 4940 trader.SearchInstruments(pattern=args.search[0], show=True) 4941 4942 elif args.info: 4943 if not (args.ticker or args.figi): 4944 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4945 raise Exception("Ticker or FIGI required") 4946 4947 if args.output is not None: 4948 trader.infoFile = args.output 4949 4950 if args.ticker: 4951 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4952 4953 else: 4954 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4955 4956 elif args.calendar is not None: 4957 if args.output is not None: 4958 trader.calendarFile = args.output 4959 4960 if len(args.calendar) == 0: 4961 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4962 4963 else: 4964 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4965 4966 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4967 4968 elif args.price: 4969 if not (args.ticker or args.figi): 4970 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4971 raise Exception("Ticker or FIGI required") 4972 4973 trader.GetCurrentPrices(show=True) 4974 4975 elif args.prices is not None: 4976 if args.output is not None: 4977 trader.pricesFile = args.output 4978 4979 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4980 4981 elif args.overview: 4982 if args.output is not None: 4983 trader.overviewFile = args.output 4984 4985 trader.Overview(show=True, details="full") 4986 4987 elif args.overview_digest: 4988 if args.output is not None: 4989 trader.overviewDigestFile = args.output 4990 4991 trader.Overview(show=True, details="digest") 4992 4993 elif args.overview_positions: 4994 if args.output is not None: 4995 trader.overviewPositionsFile = args.output 4996 4997 trader.Overview(show=True, details="positions") 4998 4999 elif args.overview_orders: 5000 if args.output is not None: 5001 trader.overviewOrdersFile = args.output 5002 5003 trader.Overview(show=True, details="orders") 5004 5005 elif args.overview_analytics: 5006 if args.output is not None: 5007 trader.overviewAnalyticsFile = args.output 5008 5009 trader.Overview(show=True, details="analytics") 5010 5011 elif args.overview_calendar: 5012 if args.output is not None: 5013 trader.overviewAnalyticsFile = args.output 5014 5015 trader.Overview(show=True, details="calendar") 5016 5017 elif args.deals is not None: 5018 if args.output is not None: 5019 trader.reportFile = args.output 5020 5021 if 0 <= len(args.deals) < 3: 5022 trader.Deals( 5023 start=args.deals[0] if len(args.deals) >= 1 else None, 5024 end=args.deals[1] if len(args.deals) == 2 else None, 5025 show=True, # Always show deals report in console 5026 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5027 ) 5028 5029 else: 5030 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5031 raise Exception("Incorrect value") 5032 5033 elif args.history is not None: 5034 if args.output is not None: 5035 trader.historyFile = args.output 5036 5037 if 0 <= len(args.history) < 3: 5038 dataReceived = trader.History( 5039 start=args.history[0] if len(args.history) >= 1 else None, 5040 end=args.history[1] if len(args.history) == 2 else None, 5041 interval="hour" if args.interval is None or not args.interval else args.interval, 5042 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5043 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5044 show=True, # shows all downloaded candles in console 5045 ) 5046 5047 if args.render_chart is not None and dataReceived is not None: 5048 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5049 5050 trader.ShowHistoryChart( 5051 candles=dataReceived, 5052 interact=iChart, 5053 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5054 ) 5055 5056 else: 5057 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5058 raise Exception("Incorrect value") 5059 5060 elif args.load_history is not None: 5061 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5062 5063 if args.render_chart is not None and histData is not None: 5064 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5065 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5066 5067 trader.ShowHistoryChart( 5068 candles=histData, 5069 interact=iChart, 5070 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5071 ) 5072 5073 elif args.trade is not None: 5074 if 1 <= len(args.trade) <= 5: 5075 trader.Trade( 5076 operation=args.trade[0], 5077 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5078 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5079 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5080 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5081 ) 5082 5083 else: 5084 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5085 5086 elif args.buy is not None: 5087 if 0 <= len(args.buy) <= 4: 5088 trader.Buy( 5089 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5090 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5091 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5092 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5093 ) 5094 5095 else: 5096 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5097 5098 elif args.sell is not None: 5099 if 0 <= len(args.sell) <= 4: 5100 trader.Sell( 5101 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5102 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5103 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5104 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5105 ) 5106 5107 else: 5108 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5109 5110 elif args.order: 5111 if 4 <= len(args.order) <= 7: 5112 trader.Order( 5113 operation=args.order[0], 5114 orderType=args.order[1], 5115 lots=int(args.order[2]), 5116 targetPrice=float(args.order[3]), 5117 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5118 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5119 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5120 ) 5121 5122 else: 5123 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5124 5125 elif args.buy_limit: 5126 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5127 5128 elif args.sell_limit: 5129 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5130 5131 elif args.buy_stop: 5132 if 2 <= len(args.buy_stop) <= 7: 5133 trader.BuyStop( 5134 lots=int(args.buy_stop[0]), 5135 targetPrice=float(args.buy_stop[1]), 5136 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5137 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5138 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5139 ) 5140 5141 else: 5142 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5143 5144 elif args.sell_stop: 5145 if 2 <= len(args.sell_stop) <= 7: 5146 trader.SellStop( 5147 lots=int(args.sell_stop[0]), 5148 targetPrice=float(args.sell_stop[1]), 5149 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5150 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5151 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5152 ) 5153 5154 else: 5155 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5156 5157 # elif args.buy_order_grid is not None: 5158 # # update order grid work with api v2 5159 # if len(args.buy_order_grid) == 2: 5160 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5161 # 5162 # for order in orderParams: 5163 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5164 # 5165 # else: 5166 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5167 # 5168 # elif args.sell_order_grid is not None: 5169 # # update order grid work with api v2 5170 # if len(args.sell_order_grid) >= 2: 5171 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5172 # 5173 # for order in orderParams: 5174 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5175 # 5176 # else: 5177 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5178 5179 elif args.close_order is not None: 5180 trader.CloseOrders(args.close_order) # close only one order 5181 5182 elif args.close_orders is not None: 5183 trader.CloseOrders(args.close_orders) # close list of orders 5184 5185 elif args.close_trade: 5186 if not (args.ticker or args.figi): 5187 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5188 raise Exception("Ticker or FIGI required") 5189 5190 if args.ticker: 5191 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5192 5193 else: 5194 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5195 5196 elif args.close_trades is not None: 5197 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5198 5199 elif args.close_all is not None: 5200 if args.ticker: 5201 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5202 5203 elif args.figi: 5204 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5205 5206 else: 5207 trader.CloseAll(*args.close_all) 5208 5209 elif args.limits: 5210 if args.output is not None: 5211 trader.withdrawalLimitsFile = args.output 5212 5213 trader.OverviewLimits(show=True) 5214 5215 elif args.user_info: 5216 if args.output is not None: 5217 trader.userInfoFile = args.output 5218 5219 trader.OverviewUserInfo(show=True) 5220 5221 elif args.account: 5222 if args.output is not None: 5223 trader.userAccountsFile = args.output 5224 5225 trader.OverviewAccounts(show=True) 5226 5227 else: 5228 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5229 raise Exception("There is no command to execute") 5230 5231 except Exception: 5232 trace = tb.format_exc() 5233 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5234 if e in trace: 5235 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5236 break 5237 5238 uLogger.debug(trace) 5239 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5240 exitCode = 255 # an error occurred, must be open a ticket for this issue 5241 5242 finally: 5243 finish = datetime.now(tzutc()) 5244 5245 if exitCode == 0: 5246 if args.more: 5247 uLogger.debug("All operations were finished success (summary code is 0).") 5248 5249 else: 5250 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5251 os.path.abspath(uLog.defaultLogFile), exitCode, 5252 )) 5253 5254 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5255 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5256 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5257 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5258 )) 5259 uLogger.debug("=-" * 50) 5260 5261 if not kwargs: 5262 sys.exit(exitCode) 5263 5264 else: 5265 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: